strix-agent 0.4.0__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 (118) hide show
  1. strix/__init__.py +0 -0
  2. strix/agents/StrixAgent/__init__.py +4 -0
  3. strix/agents/StrixAgent/strix_agent.py +89 -0
  4. strix/agents/StrixAgent/system_prompt.jinja +404 -0
  5. strix/agents/__init__.py +10 -0
  6. strix/agents/base_agent.py +518 -0
  7. strix/agents/state.py +163 -0
  8. strix/interface/__init__.py +4 -0
  9. strix/interface/assets/tui_styles.tcss +694 -0
  10. strix/interface/cli.py +230 -0
  11. strix/interface/main.py +500 -0
  12. strix/interface/tool_components/__init__.py +39 -0
  13. strix/interface/tool_components/agents_graph_renderer.py +123 -0
  14. strix/interface/tool_components/base_renderer.py +62 -0
  15. strix/interface/tool_components/browser_renderer.py +120 -0
  16. strix/interface/tool_components/file_edit_renderer.py +99 -0
  17. strix/interface/tool_components/finish_renderer.py +31 -0
  18. strix/interface/tool_components/notes_renderer.py +108 -0
  19. strix/interface/tool_components/proxy_renderer.py +255 -0
  20. strix/interface/tool_components/python_renderer.py +34 -0
  21. strix/interface/tool_components/registry.py +72 -0
  22. strix/interface/tool_components/reporting_renderer.py +53 -0
  23. strix/interface/tool_components/scan_info_renderer.py +64 -0
  24. strix/interface/tool_components/terminal_renderer.py +131 -0
  25. strix/interface/tool_components/thinking_renderer.py +29 -0
  26. strix/interface/tool_components/user_message_renderer.py +43 -0
  27. strix/interface/tool_components/web_search_renderer.py +28 -0
  28. strix/interface/tui.py +1274 -0
  29. strix/interface/utils.py +559 -0
  30. strix/llm/__init__.py +15 -0
  31. strix/llm/config.py +20 -0
  32. strix/llm/llm.py +465 -0
  33. strix/llm/memory_compressor.py +212 -0
  34. strix/llm/request_queue.py +87 -0
  35. strix/llm/utils.py +87 -0
  36. strix/prompts/README.md +64 -0
  37. strix/prompts/__init__.py +109 -0
  38. strix/prompts/cloud/.gitkeep +0 -0
  39. strix/prompts/coordination/root_agent.jinja +41 -0
  40. strix/prompts/custom/.gitkeep +0 -0
  41. strix/prompts/frameworks/fastapi.jinja +142 -0
  42. strix/prompts/frameworks/nextjs.jinja +126 -0
  43. strix/prompts/protocols/graphql.jinja +215 -0
  44. strix/prompts/reconnaissance/.gitkeep +0 -0
  45. strix/prompts/technologies/firebase_firestore.jinja +177 -0
  46. strix/prompts/technologies/supabase.jinja +189 -0
  47. strix/prompts/vulnerabilities/authentication_jwt.jinja +147 -0
  48. strix/prompts/vulnerabilities/broken_function_level_authorization.jinja +146 -0
  49. strix/prompts/vulnerabilities/business_logic.jinja +171 -0
  50. strix/prompts/vulnerabilities/csrf.jinja +174 -0
  51. strix/prompts/vulnerabilities/idor.jinja +195 -0
  52. strix/prompts/vulnerabilities/information_disclosure.jinja +222 -0
  53. strix/prompts/vulnerabilities/insecure_file_uploads.jinja +188 -0
  54. strix/prompts/vulnerabilities/mass_assignment.jinja +141 -0
  55. strix/prompts/vulnerabilities/open_redirect.jinja +177 -0
  56. strix/prompts/vulnerabilities/path_traversal_lfi_rfi.jinja +142 -0
  57. strix/prompts/vulnerabilities/race_conditions.jinja +164 -0
  58. strix/prompts/vulnerabilities/rce.jinja +154 -0
  59. strix/prompts/vulnerabilities/sql_injection.jinja +151 -0
  60. strix/prompts/vulnerabilities/ssrf.jinja +135 -0
  61. strix/prompts/vulnerabilities/subdomain_takeover.jinja +155 -0
  62. strix/prompts/vulnerabilities/xss.jinja +169 -0
  63. strix/prompts/vulnerabilities/xxe.jinja +184 -0
  64. strix/runtime/__init__.py +19 -0
  65. strix/runtime/docker_runtime.py +399 -0
  66. strix/runtime/runtime.py +29 -0
  67. strix/runtime/tool_server.py +205 -0
  68. strix/telemetry/__init__.py +4 -0
  69. strix/telemetry/tracer.py +337 -0
  70. strix/tools/__init__.py +64 -0
  71. strix/tools/agents_graph/__init__.py +16 -0
  72. strix/tools/agents_graph/agents_graph_actions.py +621 -0
  73. strix/tools/agents_graph/agents_graph_actions_schema.xml +226 -0
  74. strix/tools/argument_parser.py +121 -0
  75. strix/tools/browser/__init__.py +4 -0
  76. strix/tools/browser/browser_actions.py +236 -0
  77. strix/tools/browser/browser_actions_schema.xml +183 -0
  78. strix/tools/browser/browser_instance.py +533 -0
  79. strix/tools/browser/tab_manager.py +342 -0
  80. strix/tools/executor.py +305 -0
  81. strix/tools/file_edit/__init__.py +4 -0
  82. strix/tools/file_edit/file_edit_actions.py +141 -0
  83. strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
  84. strix/tools/finish/__init__.py +4 -0
  85. strix/tools/finish/finish_actions.py +174 -0
  86. strix/tools/finish/finish_actions_schema.xml +45 -0
  87. strix/tools/notes/__init__.py +14 -0
  88. strix/tools/notes/notes_actions.py +191 -0
  89. strix/tools/notes/notes_actions_schema.xml +150 -0
  90. strix/tools/proxy/__init__.py +20 -0
  91. strix/tools/proxy/proxy_actions.py +101 -0
  92. strix/tools/proxy/proxy_actions_schema.xml +267 -0
  93. strix/tools/proxy/proxy_manager.py +785 -0
  94. strix/tools/python/__init__.py +4 -0
  95. strix/tools/python/python_actions.py +47 -0
  96. strix/tools/python/python_actions_schema.xml +131 -0
  97. strix/tools/python/python_instance.py +172 -0
  98. strix/tools/python/python_manager.py +131 -0
  99. strix/tools/registry.py +196 -0
  100. strix/tools/reporting/__init__.py +6 -0
  101. strix/tools/reporting/reporting_actions.py +63 -0
  102. strix/tools/reporting/reporting_actions_schema.xml +30 -0
  103. strix/tools/terminal/__init__.py +4 -0
  104. strix/tools/terminal/terminal_actions.py +35 -0
  105. strix/tools/terminal/terminal_actions_schema.xml +146 -0
  106. strix/tools/terminal/terminal_manager.py +151 -0
  107. strix/tools/terminal/terminal_session.py +447 -0
  108. strix/tools/thinking/__init__.py +4 -0
  109. strix/tools/thinking/thinking_actions.py +18 -0
  110. strix/tools/thinking/thinking_actions_schema.xml +52 -0
  111. strix/tools/web_search/__init__.py +4 -0
  112. strix/tools/web_search/web_search_actions.py +80 -0
  113. strix/tools/web_search/web_search_actions_schema.xml +83 -0
  114. strix_agent-0.4.0.dist-info/LICENSE +201 -0
  115. strix_agent-0.4.0.dist-info/METADATA +282 -0
  116. strix_agent-0.4.0.dist-info/RECORD +118 -0
  117. strix_agent-0.4.0.dist-info/WHEEL +4 -0
  118. strix_agent-0.4.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,34 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class PythonRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "python_action"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "python-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+
18
+ action = args.get("action", "")
19
+ code = args.get("code", "")
20
+
21
+ header = "</> [bold #3b82f6]Python[/]"
22
+
23
+ if code and action in ["new_session", "execute"]:
24
+ code_display = code[:600] + "..." if len(code) > 600 else code
25
+ content_text = f"{header}\n [italic white]{cls.escape_markup(code_display)}[/]"
26
+ elif action == "close":
27
+ content_text = f"{header}\n [dim]Closing session...[/]"
28
+ elif action == "list_sessions":
29
+ content_text = f"{header}\n [dim]Listing sessions...[/]"
30
+ else:
31
+ content_text = f"{header}\n [dim]Running...[/]"
32
+
33
+ css_classes = cls.get_css_classes("completed")
34
+ return Static(content_text, classes=css_classes)
@@ -0,0 +1,72 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+
7
+
8
+ class ToolTUIRegistry:
9
+ _renderers: ClassVar[dict[str, type[BaseToolRenderer]]] = {}
10
+
11
+ @classmethod
12
+ def register(cls, renderer_class: type[BaseToolRenderer]) -> None:
13
+ if not renderer_class.tool_name:
14
+ raise ValueError(f"Renderer {renderer_class.__name__} must define tool_name")
15
+
16
+ cls._renderers[renderer_class.tool_name] = renderer_class
17
+
18
+ @classmethod
19
+ def get_renderer(cls, tool_name: str) -> type[BaseToolRenderer] | None:
20
+ return cls._renderers.get(tool_name)
21
+
22
+ @classmethod
23
+ def list_tools(cls) -> list[str]:
24
+ return list(cls._renderers.keys())
25
+
26
+ @classmethod
27
+ def has_renderer(cls, tool_name: str) -> bool:
28
+ return tool_name in cls._renderers
29
+
30
+
31
+ def register_tool_renderer(renderer_class: type[BaseToolRenderer]) -> type[BaseToolRenderer]:
32
+ ToolTUIRegistry.register(renderer_class)
33
+ return renderer_class
34
+
35
+
36
+ def get_tool_renderer(tool_name: str) -> type[BaseToolRenderer] | None:
37
+ return ToolTUIRegistry.get_renderer(tool_name)
38
+
39
+
40
+ def render_tool_widget(tool_data: dict[str, Any]) -> Static:
41
+ tool_name = tool_data.get("tool_name", "")
42
+ renderer = get_tool_renderer(tool_name)
43
+
44
+ if renderer:
45
+ return renderer.render(tool_data)
46
+ return _render_default_tool_widget(tool_data)
47
+
48
+
49
+ def _render_default_tool_widget(tool_data: dict[str, Any]) -> Static:
50
+ tool_name = BaseToolRenderer.escape_markup(tool_data.get("tool_name", "Unknown Tool"))
51
+ args = tool_data.get("args", {})
52
+ status = tool_data.get("status", "unknown")
53
+ result = tool_data.get("result")
54
+
55
+ status_text = BaseToolRenderer.get_status_icon(status)
56
+
57
+ header = f"→ Using tool [bold blue]{BaseToolRenderer.escape_markup(tool_name)}[/]"
58
+ content_parts = [header]
59
+
60
+ args_str = BaseToolRenderer.format_args(args)
61
+ if args_str:
62
+ content_parts.append(args_str)
63
+
64
+ if status in ["completed", "failed", "error"] and result is not None:
65
+ result_str = BaseToolRenderer.format_result(result)
66
+ if result_str:
67
+ content_parts.append(f"[bold]Result:[/] {result_str}")
68
+ else:
69
+ content_parts.append(status_text)
70
+
71
+ css_classes = BaseToolRenderer.get_css_classes(status)
72
+ return Static("\n".join(content_parts), classes=css_classes)
@@ -0,0 +1,53 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "create_vulnerability_report"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "reporting-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+
18
+ title = args.get("title", "")
19
+ severity = args.get("severity", "")
20
+ content = args.get("content", "")
21
+
22
+ header = "🐞 [bold #ea580c]Vulnerability Report[/]"
23
+
24
+ if title:
25
+ content_parts = [f"{header}\n [bold]{cls.escape_markup(title)}[/]"]
26
+
27
+ if severity:
28
+ severity_color = cls._get_severity_color(severity.lower())
29
+ content_parts.append(
30
+ f" [dim]Severity: [{severity_color}]"
31
+ f"{cls.escape_markup(severity.upper())}[/{severity_color}][/]"
32
+ )
33
+
34
+ if content:
35
+ content_parts.append(f" [dim]{cls.escape_markup(content)}[/]")
36
+
37
+ content_text = "\n".join(content_parts)
38
+ else:
39
+ content_text = f"{header}\n [dim]Creating report...[/]"
40
+
41
+ css_classes = cls.get_css_classes("completed")
42
+ return Static(content_text, classes=css_classes)
43
+
44
+ @classmethod
45
+ def _get_severity_color(cls, severity: str) -> str:
46
+ severity_colors = {
47
+ "critical": "#dc2626",
48
+ "high": "#ea580c",
49
+ "medium": "#d97706",
50
+ "low": "#65a30d",
51
+ "info": "#0284c7",
52
+ }
53
+ return severity_colors.get(severity, "#6b7280")
@@ -0,0 +1,64 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class ScanStartInfoRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "scan_start_info"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "scan-info-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+ status = tool_data.get("status", "unknown")
18
+
19
+ targets = args.get("targets", [])
20
+
21
+ if len(targets) == 1:
22
+ target_display = cls._build_single_target_display(targets[0])
23
+ content = f"🚀 Starting penetration test on {target_display}"
24
+ elif len(targets) > 1:
25
+ content = f"🚀 Starting penetration test on {len(targets)} targets"
26
+ for target_info in targets:
27
+ target_display = cls._build_single_target_display(target_info)
28
+ content += f"\n • {target_display}"
29
+ else:
30
+ content = "🚀 Starting penetration test"
31
+
32
+ css_classes = cls.get_css_classes(status)
33
+ return Static(content, classes=css_classes)
34
+
35
+ @classmethod
36
+ def _build_single_target_display(cls, target_info: dict[str, Any]) -> str:
37
+ original = target_info.get("original")
38
+ if original:
39
+ return cls.escape_markup(str(original))
40
+
41
+ return "unknown target"
42
+
43
+
44
+ @register_tool_renderer
45
+ class SubagentStartInfoRenderer(BaseToolRenderer):
46
+ tool_name: ClassVar[str] = "subagent_start_info"
47
+ css_classes: ClassVar[list[str]] = ["tool-call", "subagent-info-tool"]
48
+
49
+ @classmethod
50
+ def render(cls, tool_data: dict[str, Any]) -> Static:
51
+ args = tool_data.get("args", {})
52
+ status = tool_data.get("status", "unknown")
53
+
54
+ name = args.get("name", "Unknown Agent")
55
+ task = args.get("task", "")
56
+
57
+ name = cls.escape_markup(str(name))
58
+ content = f"🤖 Spawned subagent {name}"
59
+ if task:
60
+ task = cls.escape_markup(str(task))
61
+ content += f"\n Task: {task}"
62
+
63
+ css_classes = cls.get_css_classes(status)
64
+ return Static(content, classes=css_classes)
@@ -0,0 +1,131 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class TerminalRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "terminal_execute"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "terminal-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+ status = tool_data.get("status", "unknown")
18
+ result = tool_data.get("result", {})
19
+
20
+ command = args.get("command", "")
21
+ is_input = args.get("is_input", False)
22
+ terminal_id = args.get("terminal_id", "default")
23
+ timeout = args.get("timeout")
24
+
25
+ content = cls._build_sleek_content(command, is_input, terminal_id, timeout, result)
26
+
27
+ css_classes = cls.get_css_classes(status)
28
+ return Static(content, classes=css_classes)
29
+
30
+ @classmethod
31
+ def _build_sleek_content(
32
+ cls,
33
+ command: str,
34
+ is_input: bool,
35
+ terminal_id: str, # noqa: ARG003
36
+ timeout: float | None, # noqa: ARG003
37
+ result: dict[str, Any], # noqa: ARG003
38
+ ) -> str:
39
+ terminal_icon = ">_"
40
+
41
+ if not command.strip():
42
+ return f"{terminal_icon} [dim]getting logs...[/]"
43
+
44
+ control_sequences = {
45
+ "C-c",
46
+ "C-d",
47
+ "C-z",
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
+ }
106
+
107
+ is_special = (
108
+ command in control_sequences
109
+ or command in special_keys
110
+ or command.startswith(("M-", "S-", "C-S-", "C-M-", "S-M-"))
111
+ )
112
+
113
+ if is_special:
114
+ return f"{terminal_icon} [#ef4444]{cls.escape_markup(command)}[/]"
115
+
116
+ if is_input:
117
+ formatted_command = cls._format_command_display(command)
118
+ return f"{terminal_icon} [#3b82f6]>>>[/] [#22c55e]{formatted_command}[/]"
119
+
120
+ formatted_command = cls._format_command_display(command)
121
+ return f"{terminal_icon} [#22c55e]$ {formatted_command}[/]"
122
+
123
+ @classmethod
124
+ def _format_command_display(cls, command: str) -> str:
125
+ if not command:
126
+ return ""
127
+
128
+ if len(command) > 400:
129
+ command = command[:397] + "..."
130
+
131
+ return cls.escape_markup(command)
@@ -0,0 +1,29 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class ThinkRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "think"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "thinking-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+
18
+ thought = args.get("thought", "")
19
+
20
+ header = "🧠 [bold #a855f7]Thinking[/]"
21
+
22
+ if thought:
23
+ thought_display = thought[:600] + "..." if len(thought) > 600 else thought
24
+ content = f"{header}\n [italic dim]{cls.escape_markup(thought_display)}[/]"
25
+ else:
26
+ content = f"{header}\n [italic dim]Thinking...[/]"
27
+
28
+ css_classes = cls.get_css_classes("completed")
29
+ return Static(content, classes=css_classes)
@@ -0,0 +1,43 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class UserMessageRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "user_message"
12
+ css_classes: ClassVar[list[str]] = ["chat-message", "user-message"]
13
+
14
+ @classmethod
15
+ def render(cls, message_data: dict[str, Any]) -> Static:
16
+ content = message_data.get("content", "")
17
+
18
+ if not content:
19
+ return Static("", classes=cls.css_classes)
20
+
21
+ if len(content) > 300:
22
+ content = content[:297] + "..."
23
+
24
+ lines = content.split("\n")
25
+ bordered_lines = [f"[#3b82f6]▍[/#3b82f6] {line}" for line in lines]
26
+ bordered_content = "\n".join(bordered_lines)
27
+ formatted_content = f"[#3b82f6]▍[/#3b82f6] [bold]You:[/]\n{bordered_content}"
28
+
29
+ css_classes = " ".join(cls.css_classes)
30
+ return Static(formatted_content, classes=css_classes)
31
+
32
+ @classmethod
33
+ def render_simple(cls, content: str) -> str:
34
+ if not content:
35
+ return ""
36
+
37
+ if len(content) > 300:
38
+ content = content[:297] + "..."
39
+
40
+ lines = content.split("\n")
41
+ bordered_lines = [f"[#3b82f6]▍[/#3b82f6] {line}" for line in lines]
42
+ bordered_content = "\n".join(bordered_lines)
43
+ return f"[#3b82f6]▍[/#3b82f6] [bold]You:[/]\n{bordered_content}"
@@ -0,0 +1,28 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class WebSearchRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "web_search"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "web-search-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+ query = args.get("query", "")
18
+
19
+ header = "🌐 [bold #60a5fa]Searching the web...[/]"
20
+
21
+ if query:
22
+ query_display = query[:100] + "..." if len(query) > 100 else query
23
+ content_text = f"{header}\n [dim]{cls.escape_markup(query_display)}[/]"
24
+ else:
25
+ content_text = f"{header}"
26
+
27
+ css_classes = cls.get_css_classes("completed")
28
+ return Static(content_text, classes=css_classes)