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
@@ -0,0 +1,190 @@
1
+ from functools import cache
2
+ from typing import Any, ClassVar
3
+
4
+ from pygments.lexers import get_lexer_by_name, guess_lexer
5
+ from pygments.styles import get_style_by_name
6
+ from pygments.util import ClassNotFound
7
+ from rich.text import Text
8
+ from textual.widgets import Static
9
+
10
+ from .base_renderer import BaseToolRenderer
11
+ from .registry import register_tool_renderer
12
+
13
+
14
+ _HEADER_STYLES = [
15
+ ("###### ", 7, "bold #4ade80"),
16
+ ("##### ", 6, "bold #22c55e"),
17
+ ("#### ", 5, "bold #16a34a"),
18
+ ("### ", 4, "bold #15803d"),
19
+ ("## ", 3, "bold #22c55e"),
20
+ ("# ", 2, "bold #4ade80"),
21
+ ]
22
+
23
+
24
+ @cache
25
+ def _get_style_colors() -> dict[Any, str]:
26
+ style = get_style_by_name("native")
27
+ return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]}
28
+
29
+
30
+ def _get_token_color(token_type: Any) -> str | None:
31
+ colors = _get_style_colors()
32
+ while token_type:
33
+ if token_type in colors:
34
+ return colors[token_type]
35
+ token_type = token_type.parent
36
+ return None
37
+
38
+
39
+ def _highlight_code(code: str, language: str | None = None) -> Text:
40
+ text = Text()
41
+
42
+ try:
43
+ lexer = get_lexer_by_name(language) if language else guess_lexer(code)
44
+ except ClassNotFound:
45
+ text.append(code, style="#d4d4d4")
46
+ return text
47
+
48
+ for token_type, token_value in lexer.get_tokens(code):
49
+ if not token_value:
50
+ continue
51
+ color = _get_token_color(token_type)
52
+ text.append(token_value, style=color)
53
+
54
+ return text
55
+
56
+
57
+ def _try_parse_header(line: str) -> tuple[str, str] | None:
58
+ for prefix, strip_len, style in _HEADER_STYLES:
59
+ if line.startswith(prefix):
60
+ return (line[strip_len:], style)
61
+ return None
62
+
63
+
64
+ def _apply_markdown_styles(text: str) -> Text: # noqa: PLR0912
65
+ result = Text()
66
+ lines = text.split("\n")
67
+
68
+ in_code_block = False
69
+ code_block_lang: str | None = None
70
+ code_block_lines: list[str] = []
71
+
72
+ for i, line in enumerate(lines):
73
+ if i > 0 and not in_code_block:
74
+ result.append("\n")
75
+
76
+ if line.startswith("```"):
77
+ if not in_code_block:
78
+ in_code_block = True
79
+ code_block_lang = line[3:].strip() or None
80
+ code_block_lines = []
81
+ if i > 0:
82
+ result.append("\n")
83
+ else:
84
+ in_code_block = False
85
+ code_content = "\n".join(code_block_lines)
86
+ if code_content:
87
+ result.append_text(_highlight_code(code_content, code_block_lang))
88
+ code_block_lines = []
89
+ code_block_lang = None
90
+ continue
91
+
92
+ if in_code_block:
93
+ code_block_lines.append(line)
94
+ continue
95
+
96
+ header = _try_parse_header(line)
97
+ if header:
98
+ result.append(header[0], style=header[1])
99
+ elif line.startswith("> "):
100
+ result.append("┃ ", style="#22c55e")
101
+ result.append_text(_process_inline_formatting(line[2:]))
102
+ elif line.startswith(("- ", "* ")):
103
+ result.append("• ", style="#22c55e")
104
+ result.append_text(_process_inline_formatting(line[2:]))
105
+ elif len(line) > 2 and line[0].isdigit() and line[1:3] in (". ", ") "):
106
+ result.append(line[0] + ". ", style="#22c55e")
107
+ result.append_text(_process_inline_formatting(line[2:]))
108
+ elif line.strip() in ("---", "***", "___"):
109
+ result.append("─" * 40, style="#22c55e")
110
+ else:
111
+ result.append_text(_process_inline_formatting(line))
112
+
113
+ if in_code_block and code_block_lines:
114
+ code_content = "\n".join(code_block_lines)
115
+ result.append_text(_highlight_code(code_content, code_block_lang))
116
+
117
+ return result
118
+
119
+
120
+ def _process_inline_formatting(line: str) -> Text:
121
+ result = Text()
122
+ i = 0
123
+ n = len(line)
124
+
125
+ while i < n:
126
+ if i + 1 < n and line[i : i + 2] in ("**", "__"):
127
+ marker = line[i : i + 2]
128
+ end = line.find(marker, i + 2)
129
+ if end != -1:
130
+ result.append(line[i + 2 : end], style="bold #4ade80")
131
+ i = end + 2
132
+ continue
133
+
134
+ if i + 1 < n and line[i : i + 2] == "~~":
135
+ end = line.find("~~", i + 2)
136
+ if end != -1:
137
+ result.append(line[i + 2 : end], style="strike #525252")
138
+ i = end + 2
139
+ continue
140
+
141
+ if line[i] == "`":
142
+ end = line.find("`", i + 1)
143
+ if end != -1:
144
+ result.append(line[i + 1 : end], style="bold #22c55e on #0a0a0a")
145
+ i = end + 1
146
+ continue
147
+
148
+ if line[i] in ("*", "_"):
149
+ marker = line[i]
150
+ if i + 1 < n and line[i + 1] != marker:
151
+ end = line.find(marker, i + 1)
152
+ if end != -1 and (end + 1 >= n or line[end + 1] != marker):
153
+ result.append(line[i + 1 : end], style="italic #86efac")
154
+ i = end + 1
155
+ continue
156
+
157
+ result.append(line[i])
158
+ i += 1
159
+
160
+ return result
161
+
162
+
163
+ @register_tool_renderer
164
+ class AgentMessageRenderer(BaseToolRenderer):
165
+ tool_name: ClassVar[str] = "agent_message"
166
+ css_classes: ClassVar[list[str]] = ["chat-message", "agent-message"]
167
+
168
+ @classmethod
169
+ def render(cls, tool_data: dict[str, Any]) -> Static:
170
+ content = tool_data.get("content", "")
171
+
172
+ if not content:
173
+ return Static(Text(), classes=" ".join(cls.css_classes))
174
+
175
+ styled_text = _apply_markdown_styles(content)
176
+
177
+ return Static(styled_text, classes=" ".join(cls.css_classes))
178
+
179
+ @classmethod
180
+ def render_simple(cls, content: str) -> Text:
181
+ if not content:
182
+ return Text()
183
+
184
+ from strix.llm.utils import clean_content
185
+
186
+ cleaned = clean_content(content)
187
+ if not cleaned:
188
+ return Text()
189
+
190
+ return _apply_markdown_styles(cleaned)
@@ -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
@@ -12,11 +13,15 @@ class ViewAgentGraphRenderer(BaseToolRenderer):
12
13
  css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
13
14
 
14
15
  @classmethod
15
- def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
16
- content_text = "🕸️ [bold #fbbf24]Viewing agents graph[/]"
16
+ def render(cls, tool_data: dict[str, Any]) -> Static:
17
+ status = tool_data.get("status", "unknown")
17
18
 
18
- css_classes = cls.get_css_classes("completed")
19
- return Static(content_text, classes=css_classes)
19
+ text = Text()
20
+ text.append("◇ ", style="#a78bfa")
21
+ text.append("viewing agents graph", style="dim")
22
+
23
+ css_classes = cls.get_css_classes(status)
24
+ return Static(text, classes=css_classes)
20
25
 
21
26
 
22
27
  @register_tool_renderer
@@ -27,20 +32,22 @@ class CreateAgentRenderer(BaseToolRenderer):
27
32
  @classmethod
28
33
  def render(cls, tool_data: dict[str, Any]) -> Static:
29
34
  args = tool_data.get("args", {})
35
+ status = tool_data.get("status", "unknown")
30
36
 
31
37
  task = args.get("task", "")
32
38
  name = args.get("name", "Agent")
33
39
 
34
- header = f"🤖 [bold #fbbf24]Creating {cls.escape_markup(name)}[/]"
40
+ text = Text()
41
+ text.append("◈ ", style="#a78bfa")
42
+ text.append("spawning ", style="dim")
43
+ text.append(name, style="bold #a78bfa")
35
44
 
36
45
  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...[/]"
46
+ text.append("\n ")
47
+ text.append(task, style="dim")
41
48
 
42
- css_classes = cls.get_css_classes("completed")
43
- return Static(content_text, classes=css_classes)
49
+ css_classes = cls.get_css_classes(status)
50
+ return Static(text, classes=css_classes)
44
51
 
45
52
 
46
53
  @register_tool_renderer
@@ -51,19 +58,24 @@ class SendMessageToAgentRenderer(BaseToolRenderer):
51
58
  @classmethod
52
59
  def render(cls, tool_data: dict[str, Any]) -> Static:
53
60
  args = tool_data.get("args", {})
61
+ status = tool_data.get("status", "unknown")
54
62
 
55
63
  message = args.get("message", "")
64
+ agent_id = args.get("agent_id", "")
56
65
 
57
- header = "💬 [bold #fbbf24]Sending message[/]"
66
+ text = Text()
67
+ text.append("→ ", style="#60a5fa")
68
+ if agent_id:
69
+ text.append(f"to {agent_id}", style="dim")
70
+ else:
71
+ text.append("sending message", style="dim")
58
72
 
59
73
  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...[/]"
74
+ text.append("\n ")
75
+ text.append(message, style="dim")
64
76
 
65
- css_classes = cls.get_css_classes("completed")
66
- return Static(content_text, classes=css_classes)
77
+ css_classes = cls.get_css_classes(status)
78
+ return Static(text, classes=css_classes)
67
79
 
68
80
 
69
81
  @register_tool_renderer
@@ -79,25 +91,28 @@ class AgentFinishRenderer(BaseToolRenderer):
79
91
  findings = args.get("findings", [])
80
92
  success = args.get("success", True)
81
93
 
82
- header = (
83
- "🏁 [bold #fbbf24]Agent completed[/]" if success else "🏁 [bold #fbbf24]Agent failed[/]"
84
- )
94
+ text = Text()
95
+ text.append("🏁 ")
96
+
97
+ if success:
98
+ text.append("Agent completed", style="bold #fbbf24")
99
+ else:
100
+ text.append("Agent failed", style="bold #fbbf24")
85
101
 
86
102
  if result_summary:
87
- content_parts = [f"{header}\n [bold]{cls.escape_markup(result_summary)}[/]"]
103
+ text.append("\n ")
104
+ text.append(result_summary, style="bold")
88
105
 
89
106
  if findings and isinstance(findings, list):
90
- finding_lines = [f"• {finding}" for finding in findings]
91
- content_parts.append(
92
- f" [dim]{chr(10).join([cls.escape_markup(line) for line in finding_lines])}[/]"
93
- )
94
-
95
- content_text = "\n".join(content_parts)
107
+ for finding in findings:
108
+ text.append("\n • ")
109
+ text.append(str(finding), style="dim")
96
110
  else:
97
- content_text = f"{header}\n [dim]Completing task...[/]"
111
+ text.append("\n ")
112
+ text.append("Completing task...", style="dim")
98
113
 
99
114
  css_classes = cls.get_css_classes("completed")
100
- return Static(content_text, classes=css_classes)
115
+ return Static(text, classes=css_classes)
101
116
 
102
117
 
103
118
  @register_tool_renderer
@@ -108,16 +123,17 @@ class WaitForMessageRenderer(BaseToolRenderer):
108
123
  @classmethod
109
124
  def render(cls, tool_data: dict[str, Any]) -> Static:
110
125
  args = tool_data.get("args", {})
126
+ status = tool_data.get("status", "unknown")
111
127
 
112
- reason = args.get("reason", "Waiting for messages from other agents or user input")
128
+ reason = args.get("reason", "")
113
129
 
114
- header = "⏸️ [bold #fbbf24]Waiting for messages[/]"
130
+ text = Text()
131
+ text.append("○ ", style="#6b7280")
132
+ text.append("waiting", style="dim")
115
133
 
116
134
  if reason:
117
- reason_display = reason[:400] + "..." if len(reason) > 400 else reason
118
- content_text = f"{header}\n [dim]{cls.escape_markup(reason_display)}[/]"
119
- else:
120
- content_text = f"{header}\n [dim]Agent paused until message received...[/]"
135
+ text.append("\n ")
136
+ text.append(reason, style="dim")
121
137
 
122
- css_classes = cls.get_css_classes("completed")
123
- return Static(content_text, classes=css_classes)
138
+ css_classes = cls.get_css_classes(status)
139
+ return Static(text, classes=css_classes)
@@ -1,13 +1,12 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Any, ClassVar, cast
2
+ from typing import Any, ClassVar
3
3
 
4
- from rich.markup import escape as rich_escape
4
+ from rich.text import Text
5
5
  from textual.widgets import Static
6
6
 
7
7
 
8
8
  class BaseToolRenderer(ABC):
9
9
  tool_name: ClassVar[str] = ""
10
-
11
10
  css_classes: ClassVar[list[str]] = ["tool-call"]
12
11
 
13
12
  @classmethod
@@ -16,47 +15,80 @@ class BaseToolRenderer(ABC):
16
15
  pass
17
16
 
18
17
  @classmethod
19
- def escape_markup(cls, text: str) -> str:
20
- return cast("str", rich_escape(text))
18
+ def build_text(cls, tool_data: dict[str, Any]) -> Text: # noqa: ARG003
19
+ return Text()
21
20
 
22
21
  @classmethod
23
- def format_args(cls, args: dict[str, Any], max_length: int = 500) -> str:
24
- if not args:
25
- return ""
26
-
27
- args_parts = []
28
- for k, v in args.items():
29
- str_v = str(v)
30
- if len(str_v) > max_length:
31
- str_v = str_v[: max_length - 3] + "..."
32
- args_parts.append(f" [dim]{k}:[/] {cls.escape_markup(str_v)}")
33
- return "\n".join(args_parts)
22
+ def create_static(cls, content: Text, status: str) -> Static:
23
+ css_classes = cls.get_css_classes(status)
24
+ return Static(content, classes=css_classes)
34
25
 
35
26
  @classmethod
36
- def format_result(cls, result: Any, max_length: int = 1000) -> str:
37
- if result is None:
38
- return ""
39
-
40
- str_result = str(result).strip()
41
- if not str_result:
42
- return ""
43
-
44
- if len(str_result) > max_length:
45
- str_result = str_result[: max_length - 3] + "..."
46
- return cls.escape_markup(str_result)
47
-
48
- @classmethod
49
- def get_status_icon(cls, status: str) -> str:
50
- status_icons = {
51
- "running": "[#f59e0b]●[/#f59e0b] In progress...",
52
- "completed": "[#22c55e]✓[/#22c55e] Done",
53
- "failed": "[#dc2626]✗[/#dc2626] Failed",
54
- "error": "[#dc2626]✗[/#dc2626] Error",
27
+ def status_icon(cls, status: str) -> tuple[str, str]:
28
+ icons = {
29
+ "running": ("● In progress...", "#f59e0b"),
30
+ "completed": ("✓ Done", "#22c55e"),
31
+ "failed": ("✗ Failed", "#dc2626"),
32
+ "error": ("✗ Error", "#dc2626"),
55
33
  }
56
- return status_icons.get(status, "[dim][/dim] Unknown")
34
+ return icons.get(status, ("○ Unknown", "dim"))
57
35
 
58
36
  @classmethod
59
37
  def get_css_classes(cls, status: str) -> str:
60
38
  base_classes = cls.css_classes.copy()
61
39
  base_classes.append(f"status-{status}")
62
40
  return " ".join(base_classes)
41
+
42
+ @classmethod
43
+ def text_with_style(cls, content: str, style: str | None = None) -> Text:
44
+ text = Text()
45
+ text.append(content, style=style)
46
+ return text
47
+
48
+ @classmethod
49
+ def text_icon_label(
50
+ cls,
51
+ icon: str,
52
+ label: str,
53
+ icon_style: str | None = None,
54
+ label_style: str | None = None,
55
+ ) -> Text:
56
+ text = Text()
57
+ text.append(icon, style=icon_style)
58
+ text.append(" ")
59
+ text.append(label, style=label_style)
60
+ return text
61
+
62
+ @classmethod
63
+ def text_header(
64
+ cls,
65
+ icon: str,
66
+ title: str,
67
+ subtitle: str = "",
68
+ title_style: str = "bold",
69
+ subtitle_style: str = "dim",
70
+ ) -> Text:
71
+ text = Text()
72
+ text.append(icon)
73
+ text.append(" ")
74
+ text.append(title, style=title_style)
75
+ if subtitle:
76
+ text.append(" ")
77
+ text.append(subtitle, style=subtitle_style)
78
+ return text
79
+
80
+ @classmethod
81
+ def text_key_value(
82
+ cls,
83
+ key: str,
84
+ value: str,
85
+ key_style: str = "dim",
86
+ value_style: str | None = None,
87
+ indent: int = 2,
88
+ ) -> Text:
89
+ text = Text()
90
+ text.append(" " * indent)
91
+ text.append(key, style=key_style)
92
+ text.append(": ")
93
+ text.append(value, style=value_style)
94
+ return text