superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,923 @@
1
+ """
2
+ Unified Log Formatter for SuperQode.
3
+
4
+ Converts LogEntry objects into Rich renderables with consistent styling
5
+ across all provider modes.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import Optional, Tuple
12
+
13
+ from rich.console import RenderableType, Group
14
+ from rich.markdown import Markdown
15
+ from rich.panel import Panel
16
+ from rich.syntax import Syntax
17
+ from rich.text import Text
18
+ from rich.box import ROUNDED
19
+
20
+ from superqode.logging.unified_log import LogConfig, LogEntry, LogVerbosity
21
+
22
+
23
+ class Theme:
24
+ """Unified theme colors."""
25
+
26
+ # Primary
27
+ purple = "#a855f7"
28
+ magenta = "#d946ef"
29
+ pink = "#ec4899"
30
+ cyan = "#06b6d4"
31
+ green = "#22c55e"
32
+ orange = "#f97316"
33
+ gold = "#fbbf24"
34
+ blue = "#3b82f6"
35
+
36
+ # Status
37
+ success = "#22c55e"
38
+ error = "#ef4444"
39
+ warning = "#f59e0b"
40
+ info = "#06b6d4"
41
+
42
+ # Text
43
+ text = "#e4e4e7"
44
+ muted = "#71717a"
45
+ dim = "#52525b"
46
+
47
+ # Background
48
+ bg = "#0a0a0a"
49
+ bg_surface = "#111111"
50
+
51
+
52
+ # Thought category icons
53
+ THOUGHT_ICONS = {
54
+ "planning": "📋",
55
+ "analyzing": "🔬",
56
+ "deciding": "🤔",
57
+ "searching": "🔍",
58
+ "reading": "📖",
59
+ "writing": "✏️",
60
+ "debugging": "🐛",
61
+ "executing": "⚡",
62
+ "verifying": "✅",
63
+ "testing": "🧪",
64
+ "refactoring": "🔧",
65
+ "general": "💭",
66
+ }
67
+
68
+ # Tool icons
69
+ TOOL_ICONS = {
70
+ "read": "↳",
71
+ "write": "↲",
72
+ "edit": "⟳",
73
+ "shell": "▸",
74
+ "bash": "▸",
75
+ "search": "⌕",
76
+ "glob": "⋮",
77
+ "grep": "⌕",
78
+ "todo": "📋",
79
+ }
80
+
81
+ # Status indicators
82
+ STATUS_ICONS = {
83
+ "pending": ("○", Theme.muted),
84
+ "running": ("◐", Theme.purple),
85
+ "success": ("✦", Theme.success),
86
+ "error": ("✕", Theme.error),
87
+ }
88
+
89
+ # Language detection for syntax highlighting
90
+ LANGUAGE_PATTERNS = {
91
+ r"\.py$": "python",
92
+ r"\.js$": "javascript",
93
+ r"\.ts$": "typescript",
94
+ r"\.jsx$": "jsx",
95
+ r"\.tsx$": "tsx",
96
+ r"\.rs$": "rust",
97
+ r"\.go$": "go",
98
+ r"\.java$": "java",
99
+ r"\.rb$": "ruby",
100
+ r"\.sh$": "bash",
101
+ r"\.bash$": "bash",
102
+ r"\.zsh$": "zsh",
103
+ r"\.json$": "json",
104
+ r"\.yaml$": "yaml",
105
+ r"\.yml$": "yaml",
106
+ r"\.toml$": "toml",
107
+ r"\.md$": "markdown",
108
+ r"\.html$": "html",
109
+ r"\.css$": "css",
110
+ r"\.sql$": "sql",
111
+ }
112
+
113
+
114
+ def detect_language(file_path: str = "", content: str = "") -> str:
115
+ """Detect programming language from file path or content."""
116
+ # Try file path first
117
+ if file_path:
118
+ for pattern, lang in LANGUAGE_PATTERNS.items():
119
+ if re.search(pattern, file_path, re.IGNORECASE):
120
+ return lang
121
+
122
+ # Try content heuristics
123
+ if content:
124
+ content_lower = content.lower()
125
+ if content.startswith("#!/bin/bash") or content.startswith("#!/bin/sh"):
126
+ return "bash"
127
+ if "def " in content and ":" in content:
128
+ return "python"
129
+ if "function " in content or "const " in content or "let " in content:
130
+ return "javascript"
131
+ if "fn " in content and "->" in content:
132
+ return "rust"
133
+ if "func " in content and "{" in content:
134
+ return "go"
135
+
136
+ return "text"
137
+
138
+
139
+ def classify_thought(text: str) -> str:
140
+ """Classify thinking text into a category."""
141
+ text_lower = text.lower()
142
+
143
+ if any(w in text_lower for w in ["test", "pytest", "expect", "assert"]):
144
+ return "testing"
145
+ if any(w in text_lower for w in ["verify", "confirm", "check", "validate"]):
146
+ return "verifying"
147
+ if any(w in text_lower for w in ["run", "execute", "command", "shell"]):
148
+ return "executing"
149
+ if any(w in text_lower for w in ["debug", "error", "fix", "bug"]):
150
+ return "debugging"
151
+ if any(w in text_lower for w in ["plan", "step", "approach", "let me", "i'll"]):
152
+ return "planning"
153
+ if any(w in text_lower for w in ["analyze", "understand", "examine", "look at"]):
154
+ return "analyzing"
155
+ if any(w in text_lower for w in ["search", "find", "look for", "grep"]):
156
+ return "searching"
157
+ if any(w in text_lower for w in ["read", "file", "content"]):
158
+ return "reading"
159
+ if any(w in text_lower for w in ["write", "create", "add"]):
160
+ return "writing"
161
+ if any(w in text_lower for w in ["refactor", "restructure", "clean"]):
162
+ return "refactoring"
163
+
164
+ return "general"
165
+
166
+
167
+ def truncate(text: str, max_len: int, suffix: str = "...") -> str:
168
+ """Truncate text to max length."""
169
+ if len(text) <= max_len:
170
+ return text
171
+ return text[: max_len - len(suffix)] + suffix
172
+
173
+
174
+ class UnifiedLogFormatter:
175
+ """
176
+ Formats LogEntry objects into Rich renderables.
177
+
178
+ Provides consistent formatting across all provider modes with
179
+ support for different verbosity levels.
180
+ """
181
+
182
+ def __init__(self, config: Optional[LogConfig] = None):
183
+ self.config = config or LogConfig.normal()
184
+
185
+ def format(self, entry: LogEntry) -> Optional[RenderableType]:
186
+ """Format a log entry into a Rich renderable."""
187
+ handlers = {
188
+ "thinking": self._format_thinking,
189
+ "tool_call": self._format_tool_call,
190
+ "tool_result": self._format_tool_result,
191
+ "tool_update": self._format_tool_update,
192
+ "response_delta": self._format_response_delta,
193
+ "response_final": self._format_response_final,
194
+ "code_block": self._format_code_block,
195
+ "info": self._format_info,
196
+ "warning": self._format_warning,
197
+ "error": self._format_error,
198
+ "system": self._format_system,
199
+ "user": self._format_user,
200
+ "assistant": self._format_assistant,
201
+ }
202
+
203
+ handler = handlers.get(entry.kind)
204
+ if handler:
205
+ return handler(entry)
206
+ return None
207
+
208
+ def _format_thinking(self, entry: LogEntry) -> RenderableType:
209
+ """Format thinking/reasoning entry with engaging display."""
210
+ text = entry.text.strip()
211
+ if not text:
212
+ return Text("")
213
+
214
+ # Get category and icon
215
+ category = entry.data.get("category", classify_thought(text))
216
+ icon = THOUGHT_ICONS.get(category, "💭")
217
+
218
+ # Clean up common prefixes that make thoughts less readable
219
+ cleaned_text = text
220
+ skip_prefixes = [
221
+ "I need to ",
222
+ "I should ",
223
+ "I will ",
224
+ "I'll ",
225
+ "I'm going to ",
226
+ "Let me ",
227
+ "Now I ",
228
+ "Next I ",
229
+ "First, I ",
230
+ "Then I ",
231
+ ]
232
+ for prefix in skip_prefixes:
233
+ if cleaned_text.lower().startswith(prefix.lower()):
234
+ # Keep first letter capitalized after removing prefix
235
+ cleaned_text = cleaned_text[len(prefix) :]
236
+ if cleaned_text:
237
+ cleaned_text = cleaned_text[0].upper() + cleaned_text[1:]
238
+ break
239
+
240
+ # Truncate if needed
241
+ if self.config.verbosity == LogVerbosity.MINIMAL:
242
+ cleaned_text = truncate(cleaned_text, 50)
243
+ elif self.config.verbosity == LogVerbosity.NORMAL:
244
+ cleaned_text = truncate(cleaned_text, self.config.max_thinking_chars)
245
+
246
+ # Category-based styling for more engaging display
247
+ category_styles = {
248
+ "planning": (Theme.cyan, "Planning: "),
249
+ "analyzing": (Theme.blue, "Analyzing: "),
250
+ "debugging": (Theme.warning, "Debugging: "),
251
+ "testing": (Theme.green, "Testing: "),
252
+ "verifying": (Theme.green, "Verifying: "),
253
+ "searching": (Theme.purple, ""),
254
+ "reading": (Theme.purple, ""),
255
+ "writing": (Theme.magenta, ""),
256
+ "executing": (Theme.orange, ""),
257
+ }
258
+
259
+ style_info = category_styles.get(category)
260
+
261
+ line = Text()
262
+ line.append(f" {icon} ", style=f"bold {Theme.pink}")
263
+
264
+ if style_info and style_info[1]:
265
+ # Add category prefix for certain types
266
+ line.append(style_info[1], style=f"bold {style_info[0]}")
267
+ line.append(cleaned_text, style=f"italic {Theme.muted}")
268
+ else:
269
+ line.append(cleaned_text, style=f"italic {Theme.muted}")
270
+
271
+ return line
272
+
273
+ def _format_tool_call(self, entry: LogEntry) -> RenderableType:
274
+ """Format tool call entry with action-oriented display."""
275
+ name = entry.tool_name
276
+ args = entry.tool_args
277
+ file_path = entry.file_path
278
+ command = entry.command
279
+
280
+ # Map tool names to action verbs
281
+ action_verbs = {
282
+ "read": ("📖 Reading", Theme.cyan),
283
+ "write": ("✏️ Writing", Theme.magenta),
284
+ "edit": ("🔧 Editing", Theme.orange),
285
+ "bash": ("▸ Running", Theme.green),
286
+ "shell": ("▸ Running", Theme.green),
287
+ "search": ("🔍 Searching", Theme.purple),
288
+ "grep": ("🔍 Searching", Theme.purple),
289
+ "glob": ("📂 Finding", Theme.blue),
290
+ "todo": ("📋 Updating", Theme.cyan),
291
+ "task": ("📝 Managing", Theme.purple),
292
+ }
293
+
294
+ # Find matching action
295
+ action_text = None
296
+ action_style = Theme.purple
297
+ name_lower = name.lower()
298
+ for key, (verb, style) in action_verbs.items():
299
+ if key in name_lower:
300
+ action_text = verb
301
+ action_style = style
302
+ break
303
+
304
+ if not action_text:
305
+ action_text = f"◐ {name}"
306
+
307
+ line = Text()
308
+ line.append(f" {action_text}", style=f"bold {action_style}")
309
+
310
+ # Add context based on verbosity
311
+ if self.config.verbosity == LogVerbosity.MINIMAL:
312
+ pass # Just action
313
+ elif file_path:
314
+ # Shorten path for display
315
+ display_path = file_path
316
+ if len(display_path) > 50:
317
+ display_path = "..." + display_path[-47:]
318
+ line.append(f" {display_path}", style=Theme.muted)
319
+ elif command:
320
+ cmd_display = truncate(
321
+ command, 50 if self.config.verbosity == LogVerbosity.NORMAL else 100
322
+ )
323
+ line.append(f" $ {cmd_display}", style=Theme.muted)
324
+ elif self.config.show_tool_args and args:
325
+ # Show most relevant arg
326
+ relevant_arg = None
327
+ for key in ("path", "file_path", "pattern", "query", "command", "content"):
328
+ if key in args:
329
+ val = str(args[key])
330
+ if len(val) > 40:
331
+ val = val[:37] + "..."
332
+ relevant_arg = val
333
+ break
334
+ if relevant_arg:
335
+ line.append(f" {relevant_arg}", style=Theme.muted)
336
+ elif self.config.verbosity == LogVerbosity.VERBOSE:
337
+ args_str = str(args)[:80]
338
+ line.append(f" {args_str}", style=Theme.dim)
339
+
340
+ return line
341
+
342
+ def _format_tool_result(self, entry: LogEntry) -> RenderableType:
343
+ """Format tool result entry."""
344
+ name = entry.tool_name
345
+ success = entry.is_success
346
+ result = entry.tool_result_text
347
+
348
+ # Get tool icon
349
+ tool_icon = "•"
350
+ for key, icon in TOOL_ICONS.items():
351
+ if key in name.lower():
352
+ tool_icon = icon
353
+ break
354
+
355
+ status = "success" if success else "error"
356
+ status_icon, status_color = STATUS_ICONS.get(status, ("●", Theme.muted))
357
+
358
+ line = Text()
359
+ line.append(f" {status_icon} ", style=f"bold {status_color}")
360
+ line.append(f"{tool_icon} ", style=Theme.dim)
361
+ line.append(name, style=Theme.text)
362
+
363
+ # Add result based on verbosity
364
+ if self.config.verbosity == LogVerbosity.MINIMAL:
365
+ line.append(" done" if success else " failed", style=Theme.muted)
366
+ elif self.config.show_tool_result and result:
367
+ # Try to format as structured JSON first
368
+ formatted_json = self._format_json_result(name, result)
369
+ if formatted_json:
370
+ return Group(line, formatted_json)
371
+
372
+ # Regular result display
373
+ max_chars = self.config.max_tool_output_chars
374
+ result_display = truncate(result, max_chars)
375
+
376
+ # Check if result looks like code
377
+ if self._looks_like_code(result_display):
378
+ lang = detect_language(entry.file_path, result_display)
379
+ if self.config.syntax_highlight:
380
+ code_result = Syntax(
381
+ result_display,
382
+ lang,
383
+ theme=self.config.code_theme,
384
+ word_wrap=True,
385
+ line_numbers=False,
386
+ )
387
+ return Group(line, Text(" → ", style=Theme.muted), code_result)
388
+
389
+ if result_display:
390
+ line.append(f"\n → {result_display}", style=Theme.muted)
391
+
392
+ return line
393
+
394
+ def _format_tool_update(self, entry: LogEntry) -> RenderableType:
395
+ """Format tool update entry."""
396
+ # Similar to tool_call but for progress updates
397
+ return self._format_tool_call(entry)
398
+
399
+ def _format_response_delta(self, entry: LogEntry) -> RenderableType:
400
+ """Format streaming response chunk."""
401
+ text = entry.text
402
+ if not text:
403
+ return Text("")
404
+
405
+ # For streaming, just show plain text (final will render markdown)
406
+ return Text(text, style=Theme.text)
407
+
408
+ def _format_response_final(self, entry: LogEntry) -> RenderableType:
409
+ """Format final complete response."""
410
+ text = entry.text
411
+ if not text:
412
+ return Text("")
413
+
414
+ # Render as markdown with syntax highlighting for code blocks
415
+ if "```" in text:
416
+ return Markdown(
417
+ text,
418
+ code_theme=self.config.code_theme,
419
+ inline_code_lexer="python",
420
+ )
421
+
422
+ return Text(text, style=Theme.text)
423
+
424
+ def _format_code_block(self, entry: LogEntry) -> RenderableType:
425
+ """Format a code block with syntax highlighting."""
426
+ code = entry.text
427
+ language = entry.data.get("language", "")
428
+
429
+ if not language:
430
+ language = detect_language(content=code)
431
+
432
+ if self.config.syntax_highlight:
433
+ return Syntax(
434
+ code,
435
+ language,
436
+ theme=self.config.code_theme,
437
+ word_wrap=True,
438
+ line_numbers=True,
439
+ )
440
+
441
+ return Text(code, style=Theme.text)
442
+
443
+ def _format_info(self, entry: LogEntry) -> RenderableType:
444
+ """Format info message."""
445
+ return Text(f" ℹ️ {entry.text}", style=Theme.cyan)
446
+
447
+ def _format_warning(self, entry: LogEntry) -> RenderableType:
448
+ """Format warning message."""
449
+ return Text(f" ⚠️ {entry.text}", style=Theme.warning)
450
+
451
+ def _format_error(self, entry: LogEntry) -> RenderableType:
452
+ """Format error message."""
453
+ return Text(f" ❌ {entry.text}", style=Theme.error)
454
+
455
+ def _format_system(self, entry: LogEntry) -> RenderableType:
456
+ """Format system message."""
457
+ return Text(f" ✨ {entry.text}", style=f"italic {Theme.muted}")
458
+
459
+ def _format_user(self, entry: LogEntry) -> RenderableType:
460
+ """Format user message."""
461
+ return Panel(
462
+ Text(entry.text, style=Theme.text, overflow="fold"),
463
+ title=f"[bold {Theme.cyan}]👩‍💻👨‍💻 >[/]",
464
+ border_style=Theme.dim,
465
+ box=ROUNDED,
466
+ padding=(0, 1),
467
+ )
468
+
469
+ def _format_assistant(self, entry: LogEntry) -> RenderableType:
470
+ """Format assistant message."""
471
+ text = entry.text
472
+ agent = entry.agent or "Assistant"
473
+
474
+ content = (
475
+ Markdown(text, code_theme=self.config.code_theme)
476
+ if "```" in text
477
+ else Text(text, style=Theme.text, overflow="fold")
478
+ )
479
+
480
+ return Panel(
481
+ content,
482
+ title=f"[bold {Theme.purple}]🤖 {agent}[/]",
483
+ border_style=Theme.purple,
484
+ box=ROUNDED,
485
+ padding=(0, 1),
486
+ )
487
+
488
+ def _format_json_result(self, tool_name: str, result: str) -> Optional[RenderableType]:
489
+ """
490
+ Intelligently format JSON tool results into engaging displays.
491
+
492
+ Handles various JSON structures:
493
+ - TODO lists
494
+ - File lists (search results)
495
+ - Task definitions
496
+ - Error reports
497
+ - Generic objects/arrays
498
+ """
499
+ import json
500
+
501
+ # Skip if not JSON
502
+ result_stripped = result.strip()
503
+ if not result_stripped or (
504
+ not result_stripped.startswith("{") and not result_stripped.startswith("[")
505
+ ):
506
+ return None
507
+
508
+ try:
509
+ data = json.loads(result)
510
+ except json.JSONDecodeError:
511
+ return None
512
+
513
+ # Route to specific formatters based on tool name or data structure
514
+ tool_lower = tool_name.lower()
515
+
516
+ # TODO/Task list
517
+ if "todo" in tool_lower or self._looks_like_todo_list(data):
518
+ return self._format_todo_list(data)
519
+
520
+ # File search results
521
+ if any(x in tool_lower for x in ("glob", "search", "find", "grep")):
522
+ return self._format_file_results(data)
523
+
524
+ # Read/Write file result
525
+ if any(x in tool_lower for x in ("read", "write", "edit")):
526
+ return self._format_file_operation(data)
527
+
528
+ # Task list (Claude Code style)
529
+ if "task" in tool_lower and isinstance(data, list):
530
+ return self._format_task_list(data)
531
+
532
+ # Error/diagnostic result
533
+ if isinstance(data, dict) and ("error" in data or "errors" in data):
534
+ return self._format_error_result(data)
535
+
536
+ # Plan entries
537
+ if isinstance(data, list) and data and isinstance(data[0], dict) and "step" in data[0]:
538
+ return self._format_plan_entries(data)
539
+
540
+ # Generic list of items with identifiable structure
541
+ if isinstance(data, list) and data:
542
+ return self._format_generic_list(data)
543
+
544
+ # Generic dict
545
+ if isinstance(data, dict):
546
+ return self._format_generic_dict(data)
547
+
548
+ return None
549
+
550
+ def _looks_like_todo_list(self, data) -> bool:
551
+ """Check if data looks like a TODO list."""
552
+ if not isinstance(data, list):
553
+ return False
554
+ if not data:
555
+ return False
556
+ first = data[0]
557
+ if not isinstance(first, dict):
558
+ return False
559
+ return any(k in first for k in ("status", "title", "priority", "completed"))
560
+
561
+ def _format_todo_list(self, data) -> Optional[RenderableType]:
562
+ """Format TODO list with visual status indicators."""
563
+ if not isinstance(data, list):
564
+ if isinstance(data, dict) and "todos" in data:
565
+ data = data["todos"]
566
+ else:
567
+ return None
568
+
569
+ if not data:
570
+ return Text(" 📋 No tasks", style=Theme.muted)
571
+
572
+ lines = []
573
+ for item in data:
574
+ if not isinstance(item, dict):
575
+ continue
576
+
577
+ status = item.get("status", "pending")
578
+ title = item.get("title", item.get("name", item.get("description", str(item))))
579
+ priority = item.get("priority", "normal")
580
+
581
+ # Status icons with colors
582
+ status_icons = {
583
+ "completed": ("✅", Theme.success),
584
+ "done": ("✅", Theme.success),
585
+ "in_progress": ("🔄", Theme.purple),
586
+ "active": ("🔄", Theme.purple),
587
+ "pending": ("○", Theme.muted),
588
+ "blocked": ("🚫", Theme.error),
589
+ "failed": ("✕", Theme.error),
590
+ }
591
+ icon, icon_style = status_icons.get(status, ("○", Theme.muted))
592
+
593
+ # Priority styling
594
+ title_style = Theme.text
595
+ if priority in ("high", "important"):
596
+ title_style = Theme.warning
597
+ elif priority in ("critical", "urgent"):
598
+ title_style = Theme.error
599
+ elif status in ("completed", "done"):
600
+ title_style = Theme.muted
601
+
602
+ line = Text()
603
+ line.append(f" {icon} ", style=icon_style)
604
+ line.append(str(title)[:80], style=title_style)
605
+ lines.append(line)
606
+
607
+ # Summary line
608
+ completed = sum(
609
+ 1 for t in data if isinstance(t, dict) and t.get("status") in ("completed", "done")
610
+ )
611
+ in_progress = sum(
612
+ 1 for t in data if isinstance(t, dict) and t.get("status") in ("in_progress", "active")
613
+ )
614
+ pending = sum(
615
+ 1 for t in data if isinstance(t, dict) and t.get("status") in ("pending", None)
616
+ )
617
+
618
+ parts = []
619
+ if completed:
620
+ parts.append(f"✅ {completed}")
621
+ if in_progress:
622
+ parts.append(f"🔄 {in_progress}")
623
+ if pending:
624
+ parts.append(f"○ {pending}")
625
+
626
+ summary = Text()
627
+ summary.append(" 📋 ", style=Theme.cyan)
628
+ summary.append(f"Tasks: {' · '.join(parts) if parts else 'none'}", style=Theme.text)
629
+
630
+ return Group(summary, *lines[:10]) # Limit to 10 items
631
+
632
+ def _format_file_results(self, data) -> Optional[RenderableType]:
633
+ """Format file search/glob results."""
634
+ files = []
635
+
636
+ if isinstance(data, list):
637
+ files = [str(f) for f in data if f]
638
+ elif isinstance(data, dict):
639
+ files = data.get("files", data.get("matches", data.get("results", [])))
640
+ if not isinstance(files, list):
641
+ return None
642
+
643
+ if not files:
644
+ return Text(" 🔍 No matches found", style=Theme.muted)
645
+
646
+ lines = []
647
+ for i, f in enumerate(files[:8]): # Show max 8 files
648
+ line = Text()
649
+ line.append(" ", style=Theme.dim)
650
+ # File icon based on extension
651
+ ext = str(f).split(".")[-1].lower() if "." in str(f) else ""
652
+ icon = "📄"
653
+ if ext in ("py", "pyw"):
654
+ icon = "🐍"
655
+ elif ext in ("js", "ts", "jsx", "tsx"):
656
+ icon = "📜"
657
+ elif ext in ("rs",):
658
+ icon = "🦀"
659
+ elif ext in ("go",):
660
+ icon = "🐹"
661
+ elif ext in ("md", "txt", "rst"):
662
+ icon = "📝"
663
+ elif ext in ("json", "yaml", "yml", "toml"):
664
+ icon = "⚙️"
665
+
666
+ line.append(f"{icon} ", style=Theme.dim)
667
+ # Truncate long paths
668
+ path_str = str(f)
669
+ if len(path_str) > 60:
670
+ path_str = "..." + path_str[-57:]
671
+ line.append(path_str, style=Theme.cyan)
672
+ lines.append(line)
673
+
674
+ if len(files) > 8:
675
+ more = Text()
676
+ more.append(f" ... and {len(files) - 8} more", style=Theme.muted)
677
+ lines.append(more)
678
+
679
+ header = Text()
680
+ header.append(" 🔍 ", style=Theme.cyan)
681
+ header.append(f"Found {len(files)} file{'s' if len(files) != 1 else ''}", style=Theme.text)
682
+
683
+ return Group(header, *lines)
684
+
685
+ def _format_file_operation(self, data) -> Optional[RenderableType]:
686
+ """Format read/write/edit file results."""
687
+ if not isinstance(data, dict):
688
+ return None
689
+
690
+ path = data.get("path", data.get("file", ""))
691
+ success = data.get("success", data.get("ok", True))
692
+ content = data.get("content", data.get("data", ""))
693
+ lines_affected = data.get("lines", data.get("lines_changed", 0))
694
+
695
+ result = Text()
696
+ result.append(" ", style=Theme.dim)
697
+
698
+ if success:
699
+ result.append("✓ ", style=Theme.success)
700
+ if path:
701
+ result.append(f"{path}", style=Theme.cyan)
702
+ if lines_affected:
703
+ result.append(f" ({lines_affected} lines)", style=Theme.muted)
704
+ else:
705
+ result.append("✕ ", style=Theme.error)
706
+ error = data.get("error", "Operation failed")
707
+ result.append(str(error)[:60], style=Theme.error)
708
+
709
+ return result
710
+
711
+ def _format_task_list(self, data) -> Optional[RenderableType]:
712
+ """Format Claude Code style task lists."""
713
+ if not isinstance(data, list):
714
+ return None
715
+
716
+ lines = []
717
+ for task in data[:5]: # Limit display
718
+ if not isinstance(task, dict):
719
+ continue
720
+
721
+ status = task.get("status", "pending")
722
+ subject = task.get("subject", task.get("title", ""))
723
+ task_id = task.get("id", "")
724
+
725
+ icons = {
726
+ "completed": ("✅", Theme.success),
727
+ "in_progress": ("⏳", Theme.purple),
728
+ "pending": ("○", Theme.muted),
729
+ }
730
+ icon, style = icons.get(status, ("○", Theme.muted))
731
+
732
+ line = Text()
733
+ line.append(f" {icon} ", style=style)
734
+ if task_id:
735
+ line.append(f"[{task_id}] ", style=Theme.dim)
736
+ line.append(
737
+ str(subject)[:60], style=Theme.text if status != "completed" else Theme.muted
738
+ )
739
+ lines.append(line)
740
+
741
+ header = Text()
742
+ header.append(" 📝 ", style=Theme.purple)
743
+ header.append(f"{len(data)} task{'s' if len(data) != 1 else ''}", style=Theme.text)
744
+
745
+ return Group(header, *lines)
746
+
747
+ def _format_error_result(self, data) -> Optional[RenderableType]:
748
+ """Format error/diagnostic results."""
749
+ errors = data.get("errors", [data.get("error")]) if isinstance(data, dict) else []
750
+ if not errors:
751
+ return None
752
+
753
+ lines = []
754
+ for err in errors[:5]:
755
+ line = Text()
756
+ line.append(" ✕ ", style=Theme.error)
757
+ if isinstance(err, dict):
758
+ msg = err.get("message", err.get("error", str(err)))
759
+ loc = err.get("location", err.get("file", ""))
760
+ line.append(str(msg)[:60], style=Theme.error)
761
+ if loc:
762
+ line.append(f" @ {loc}", style=Theme.muted)
763
+ else:
764
+ line.append(str(err)[:70], style=Theme.error)
765
+ lines.append(line)
766
+
767
+ header = Text()
768
+ header.append(" ⚠️ ", style=Theme.error)
769
+ header.append(f"{len(errors)} error{'s' if len(errors) != 1 else ''}", style=Theme.error)
770
+
771
+ return Group(header, *lines)
772
+
773
+ def _format_plan_entries(self, data) -> Optional[RenderableType]:
774
+ """Format plan/step entries."""
775
+ if not isinstance(data, list):
776
+ return None
777
+
778
+ lines = []
779
+ for i, step in enumerate(data[:6], 1):
780
+ if not isinstance(step, dict):
781
+ continue
782
+
783
+ desc = step.get("description", step.get("step", step.get("action", "")))
784
+ status = step.get("status", "pending")
785
+
786
+ icons = {
787
+ "completed": "✓",
788
+ "done": "✓",
789
+ "in_progress": "→",
790
+ "active": "→",
791
+ "pending": str(i),
792
+ }
793
+ icon = icons.get(status, str(i))
794
+ style = Theme.success if status in ("completed", "done") else Theme.text
795
+
796
+ line = Text()
797
+ line.append(
798
+ f" {icon}. ",
799
+ style=Theme.purple if status not in ("completed", "done") else Theme.success,
800
+ )
801
+ line.append(str(desc)[:65], style=style)
802
+ lines.append(line)
803
+
804
+ header = Text()
805
+ header.append(" 📋 ", style=Theme.purple)
806
+ header.append("Plan:", style=Theme.text)
807
+
808
+ return Group(header, *lines)
809
+
810
+ def _format_generic_list(self, data) -> Optional[RenderableType]:
811
+ """Format a generic list of items."""
812
+ if not data:
813
+ return None
814
+
815
+ lines = []
816
+ for item in data[:6]:
817
+ line = Text()
818
+ line.append(" • ", style=Theme.dim)
819
+
820
+ if isinstance(item, dict):
821
+ # Try to find a display field
822
+ display = None
823
+ for key in ("name", "title", "path", "message", "value", "text", "description"):
824
+ if key in item:
825
+ display = item[key]
826
+ break
827
+ if display is None:
828
+ display = str(item)
829
+ line.append(str(display)[:70], style=Theme.text)
830
+ else:
831
+ line.append(str(item)[:70], style=Theme.text)
832
+
833
+ lines.append(line)
834
+
835
+ if len(data) > 6:
836
+ more = Text()
837
+ more.append(f" ... and {len(data) - 6} more items", style=Theme.muted)
838
+ lines.append(more)
839
+
840
+ return Group(*lines) if lines else None
841
+
842
+ def _format_generic_dict(self, data) -> Optional[RenderableType]:
843
+ """Format a generic dictionary result."""
844
+ if not data:
845
+ return None
846
+
847
+ # Check for common success/result patterns
848
+ if "success" in data or "ok" in data or "result" in data:
849
+ success = data.get("success", data.get("ok", True))
850
+ result_val = data.get("result", data.get("message", data.get("output", "")))
851
+
852
+ line = Text()
853
+ line.append(" ", style=Theme.dim)
854
+ if success:
855
+ line.append("✓ ", style=Theme.success)
856
+ if result_val:
857
+ line.append(str(result_val)[:60], style=Theme.text)
858
+ else:
859
+ line.append("Success", style=Theme.success)
860
+ else:
861
+ line.append("✕ ", style=Theme.error)
862
+ error = data.get("error", data.get("message", "Failed"))
863
+ line.append(str(error)[:60], style=Theme.error)
864
+ return line
865
+
866
+ # Show key-value pairs for small dicts
867
+ if len(data) <= 4:
868
+ lines = []
869
+ for key, val in list(data.items())[:4]:
870
+ line = Text()
871
+ line.append(" ", style=Theme.dim)
872
+ line.append(f"{key}: ", style=Theme.purple)
873
+ val_str = str(val)
874
+ if len(val_str) > 50:
875
+ val_str = val_str[:47] + "..."
876
+ line.append(val_str, style=Theme.text)
877
+ lines.append(line)
878
+ return Group(*lines) if lines else None
879
+
880
+ # For larger dicts, just show a summary
881
+ line = Text()
882
+ line.append(" ", style=Theme.dim)
883
+ line.append("{ ", style=Theme.muted)
884
+ keys = list(data.keys())[:4]
885
+ line.append(", ".join(keys), style=Theme.text)
886
+ if len(data) > 4:
887
+ line.append(f", ... +{len(data) - 4}", style=Theme.muted)
888
+ line.append(" }", style=Theme.muted)
889
+ return line
890
+
891
+ def _format_todo_result(self, result: str) -> Optional[RenderableType]:
892
+ """Format todo list result nicely. (Legacy - redirects to new formatter)"""
893
+ try:
894
+ import json
895
+
896
+ data = json.loads(result)
897
+ return self._format_todo_list(data)
898
+ except (json.JSONDecodeError, TypeError):
899
+ return None
900
+
901
+ def _looks_like_code(self, text: str) -> bool:
902
+ """Check if text looks like code."""
903
+ indicators = [
904
+ "def ",
905
+ "class ",
906
+ "function ",
907
+ "const ",
908
+ "let ",
909
+ "var ",
910
+ "import ",
911
+ "from ",
912
+ "fn ",
913
+ "func ",
914
+ "pub ",
915
+ "async ",
916
+ "await ",
917
+ "return ",
918
+ "{",
919
+ "}",
920
+ "=>",
921
+ "->",
922
+ ]
923
+ return any(ind in text for ind in indicators) and len(text) > 20