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,341 @@
1
+ """
2
+ TUI Integration for SuperQode Unified Logging.
3
+
4
+ Provides easy integration with the Textual TUI application.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Any, Callable, Optional
10
+ import asyncio
11
+
12
+ from superqode.logging.unified_log import (
13
+ LogConfig,
14
+ LogEntry,
15
+ LogSource,
16
+ LogVerbosity,
17
+ UnifiedLogger,
18
+ )
19
+ from superqode.logging.sinks import ConversationLogSink
20
+ from superqode.logging.adapters import BYOKAdapter, LocalAdapter, ACPAdapter
21
+
22
+ if TYPE_CHECKING:
23
+ from superqode.app.widgets import ConversationLog
24
+
25
+
26
+ class TUILoggerManager:
27
+ """
28
+ Manages unified logging for the TUI application.
29
+
30
+ Provides thread-safe logging callbacks for all provider modes.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ log_widget: "ConversationLog",
36
+ source: LogSource = "byok",
37
+ call_from_thread: Optional[Callable] = None,
38
+ ):
39
+ self.log_widget = log_widget
40
+ self.source = source
41
+ self._call_from_thread = call_from_thread
42
+
43
+ # Determine config based on source
44
+ self.config = LogConfig.for_source(source)
45
+
46
+ # Create logger with sink
47
+ self.logger = UnifiedLogger(config=self.config)
48
+ self.sink = ConversationLogSink(log_widget)
49
+ self.logger.add_sink(self.sink)
50
+
51
+ # Create appropriate adapter
52
+ if source == "acp":
53
+ self.adapter = ACPAdapter(self.logger)
54
+ elif source == "local":
55
+ self.adapter = LocalAdapter(self.logger)
56
+ else:
57
+ self.adapter = BYOKAdapter(self.logger)
58
+
59
+ # Buffers for ACP mode to accumulate streaming content
60
+ self._thinking_buffer = ""
61
+ self._thinking_flush_task: Optional[asyncio.Task] = None
62
+ self._thinking_flush_delay = 0.15 # Flush after 150ms of no new chunks
63
+
64
+ def _safe_emit(self, entry: LogEntry) -> None:
65
+ """Emit entry safely from any thread."""
66
+
67
+ def _do_emit():
68
+ if self.logger._should_emit(entry):
69
+ self.sink.emit(entry, self.config)
70
+
71
+ if self._call_from_thread:
72
+ try:
73
+ self._call_from_thread(_do_emit)
74
+ except RuntimeError as e:
75
+ if "different thread" in str(e).lower():
76
+ _do_emit()
77
+ else:
78
+ raise
79
+ else:
80
+ _do_emit()
81
+
82
+ def set_verbosity(self, verbosity: LogVerbosity) -> None:
83
+ """Change verbosity level."""
84
+ self.logger.set_verbosity(verbosity)
85
+
86
+ def toggle_thinking(self) -> bool:
87
+ """Toggle thinking display. Returns new state."""
88
+ return self.logger.toggle_thinking()
89
+
90
+ def get_byok_callbacks(self) -> dict[str, Callable]:
91
+ """
92
+ Get callbacks for BYOK mode that emit through unified logging.
93
+
94
+ Returns dict with: on_tool_call, on_tool_result, on_thinking
95
+ """
96
+ adapter = (
97
+ self.adapter if isinstance(self.adapter, BYOKAdapter) else BYOKAdapter(self.logger)
98
+ )
99
+
100
+ def on_tool_call(name: str, args: dict) -> None:
101
+ entry = LogEntry.tool_call(name, args, source="byok")
102
+ self._safe_emit(entry)
103
+ adapter._span_ids[name] = entry.span_id or ""
104
+
105
+ def on_tool_result(name: str, result: Any) -> None:
106
+ from superqode.tools.base import ToolResult
107
+
108
+ span_id = adapter._span_ids.pop(name, None)
109
+
110
+ if isinstance(result, ToolResult):
111
+ success = result.success
112
+ output = str(result.output) if result.output else ""
113
+ if not success and result.error:
114
+ output = str(result.error)
115
+ else:
116
+ success = True
117
+ output = str(result) if result else ""
118
+
119
+ entry = LogEntry.tool_result(name, output, success, source="byok", span_id=span_id)
120
+ self._safe_emit(entry)
121
+
122
+ async def on_thinking(text: str) -> None:
123
+ if text and text.strip():
124
+ entry = LogEntry.thinking(text, source="byok")
125
+ self._safe_emit(entry)
126
+
127
+ return {
128
+ "on_tool_call": on_tool_call,
129
+ "on_tool_result": on_tool_result,
130
+ "on_thinking": on_thinking,
131
+ }
132
+
133
+ def get_local_callbacks(self) -> dict[str, Callable]:
134
+ """
135
+ Get callbacks for Local mode (Ollama, etc.).
136
+
137
+ Same as BYOK but with 'local' source for different default config.
138
+ """
139
+ adapter = (
140
+ self.adapter if isinstance(self.adapter, LocalAdapter) else LocalAdapter(self.logger)
141
+ )
142
+
143
+ def on_tool_call(name: str, args: dict) -> None:
144
+ entry = LogEntry.tool_call(name, args, source="local")
145
+ self._safe_emit(entry)
146
+
147
+ def on_tool_result(name: str, result: Any) -> None:
148
+ from superqode.tools.base import ToolResult
149
+
150
+ if isinstance(result, ToolResult):
151
+ success = result.success
152
+ output = str(result.output) if result.output else ""
153
+ if not success and result.error:
154
+ output = str(result.error)
155
+ else:
156
+ success = True
157
+ output = str(result) if result else ""
158
+
159
+ entry = LogEntry.tool_result(name, output, success, source="local")
160
+ self._safe_emit(entry)
161
+
162
+ async def on_thinking(text: str) -> None:
163
+ if text and text.strip():
164
+ entry = LogEntry.thinking(text, source="local")
165
+ self._safe_emit(entry)
166
+
167
+ return {
168
+ "on_tool_call": on_tool_call,
169
+ "on_tool_result": on_tool_result,
170
+ "on_thinking": on_thinking,
171
+ }
172
+
173
+ def _flush_thinking_buffer(self) -> None:
174
+ """Flush accumulated thinking buffer to display."""
175
+ if self._thinking_buffer.strip():
176
+ # Clean ACP prefixes
177
+ clean_text = self._thinking_buffer
178
+ if clean_text.startswith("[agent] "):
179
+ clean_text = clean_text[8:]
180
+ elif clean_text.startswith("["):
181
+ bracket_end = clean_text.find("] ")
182
+ if bracket_end > 0:
183
+ clean_text = clean_text[bracket_end + 2 :]
184
+
185
+ # Only emit if we have meaningful content
186
+ clean_text = clean_text.strip()
187
+ if clean_text:
188
+ entry = LogEntry.thinking(clean_text, source="acp")
189
+ self._safe_emit(entry)
190
+
191
+ self._thinking_buffer = ""
192
+ self._thinking_flush_task = None
193
+
194
+ async def _schedule_thinking_flush(self) -> None:
195
+ """Schedule a flush after delay if no new chunks arrive."""
196
+ await asyncio.sleep(self._thinking_flush_delay)
197
+ self._flush_thinking_buffer()
198
+
199
+ def get_acp_callbacks(self) -> dict[str, Callable]:
200
+ """
201
+ Get callbacks for ACP mode.
202
+
203
+ Returns dict with: on_message, on_thinking, on_tool_call, on_tool_update
204
+ """
205
+ adapter = self.adapter if isinstance(self.adapter, ACPAdapter) else ACPAdapter(self.logger)
206
+
207
+ async def on_message(text: str) -> None:
208
+ if text:
209
+ adapter._message_buffer += text
210
+ entry = LogEntry.response(text, source="acp", agent="Agent", is_final=False)
211
+ self._safe_emit(entry)
212
+
213
+ async def on_thinking(text: str) -> None:
214
+ """Buffer thinking chunks and emit complete thoughts."""
215
+ if not text:
216
+ return
217
+
218
+ # Add to buffer
219
+ self._thinking_buffer += text
220
+
221
+ # Cancel any pending flush
222
+ if self._thinking_flush_task and not self._thinking_flush_task.done():
223
+ self._thinking_flush_task.cancel()
224
+
225
+ # Check if we have a natural break point (sentence end, newline)
226
+ if self._thinking_buffer.rstrip().endswith((".", "!", "?", "\n", ":", ";")):
227
+ # Flush immediately on sentence boundaries
228
+ self._flush_thinking_buffer()
229
+ else:
230
+ # Schedule delayed flush
231
+ try:
232
+ self._thinking_flush_task = asyncio.create_task(self._schedule_thinking_flush())
233
+ except RuntimeError:
234
+ # No event loop - flush immediately
235
+ self._flush_thinking_buffer()
236
+
237
+ async def on_tool_call(tool_call: dict) -> None:
238
+ # Flush any pending thinking before tool call
239
+ if self._thinking_buffer:
240
+ self._flush_thinking_buffer()
241
+
242
+ title = tool_call.get("title", "tool")
243
+ raw_input = tool_call.get("rawInput", {})
244
+ tool_call_id = tool_call.get("toolCallId", "")
245
+
246
+ entry = LogEntry.tool_call(title, raw_input, source="acp")
247
+ if tool_call_id:
248
+ adapter._span_ids[tool_call_id] = entry.span_id or ""
249
+ self._safe_emit(entry)
250
+
251
+ async def on_tool_update(update: dict) -> None:
252
+ status = update.get("status", "")
253
+ tool_call_id = update.get("toolCallId", "")
254
+ output = update.get("rawOutput") or update.get("output") or update.get("result")
255
+ title = update.get("title", "tool")
256
+
257
+ span_id = adapter._span_ids.get(tool_call_id)
258
+
259
+ if status in ("completed", "done", "success"):
260
+ entry = LogEntry.tool_result(
261
+ title, str(output) if output else "", True, source="acp", span_id=span_id
262
+ )
263
+ self._safe_emit(entry)
264
+ elif status in ("error", "failed"):
265
+ entry = LogEntry.tool_result(
266
+ title, str(output) if output else "failed", False, source="acp", span_id=span_id
267
+ )
268
+ self._safe_emit(entry)
269
+
270
+ return {
271
+ "on_message": on_message,
272
+ "on_thinking": on_thinking,
273
+ "on_tool_call": on_tool_call,
274
+ "on_tool_update": on_tool_update,
275
+ }
276
+
277
+ def log_thinking(self, text: str, category: str = "general") -> None:
278
+ """Log a thinking entry."""
279
+ entry = LogEntry.thinking(text, source=self.source, category=category)
280
+ self._safe_emit(entry)
281
+
282
+ def log_tool_call(self, name: str, args: dict) -> str:
283
+ """Log a tool call. Returns span_id."""
284
+ entry = LogEntry.tool_call(name, args, source=self.source)
285
+ self._safe_emit(entry)
286
+ return entry.span_id or ""
287
+
288
+ def log_tool_result(
289
+ self, name: str, result: Any, success: bool = True, span_id: Optional[str] = None
290
+ ) -> None:
291
+ """Log a tool result."""
292
+ entry = LogEntry.tool_result(
293
+ name, str(result), success, source=self.source, span_id=span_id
294
+ )
295
+ self._safe_emit(entry)
296
+
297
+ def log_response_chunk(self, text: str, agent: str = "Assistant") -> None:
298
+ """Log a response chunk."""
299
+ entry = LogEntry.response(text, source=self.source, agent=agent, is_final=False)
300
+ self._safe_emit(entry)
301
+
302
+ def log_info(self, text: str) -> None:
303
+ """Log an info message."""
304
+ entry = LogEntry.info(text, source=self.source)
305
+ self._safe_emit(entry)
306
+
307
+ def log_error(self, text: str) -> None:
308
+ """Log an error message."""
309
+ entry = LogEntry.error(text, source=self.source)
310
+ self._safe_emit(entry)
311
+
312
+ def log_code_block(self, code: str, language: str = "") -> None:
313
+ """Log a code block with syntax highlighting."""
314
+ entry = LogEntry.code_block(code, language, source=self.source)
315
+ self._safe_emit(entry)
316
+
317
+
318
+ def create_tui_logger(
319
+ log_widget: "ConversationLog",
320
+ source: LogSource = "byok",
321
+ call_from_thread: Optional[Callable] = None,
322
+ verbosity: Optional[LogVerbosity] = None,
323
+ ) -> TUILoggerManager:
324
+ """
325
+ Create a TUI logger manager for the given source.
326
+
327
+ Args:
328
+ log_widget: The ConversationLog widget to write to
329
+ source: The provider source ("acp", "byok", or "local")
330
+ call_from_thread: Optional thread-safe call function (e.g., app.call_from_thread)
331
+ verbosity: Optional verbosity override
332
+
333
+ Returns:
334
+ TUILoggerManager configured for the source
335
+ """
336
+ manager = TUILoggerManager(log_widget, source, call_from_thread)
337
+
338
+ if verbosity:
339
+ manager.set_verbosity(verbosity)
340
+
341
+ return manager
@@ -0,0 +1,170 @@
1
+ """
2
+ Log Sinks for SuperQode.
3
+
4
+ Provides different output destinations for log entries.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Any, Optional
10
+
11
+ from superqode.logging.unified_log import LogConfig, LogEntry
12
+ from superqode.logging.formatter import UnifiedLogFormatter
13
+
14
+ if TYPE_CHECKING:
15
+ from superqode.app.widgets import ConversationLog
16
+
17
+
18
+ class ConversationLogSink:
19
+ """
20
+ Sink that writes to a ConversationLog widget.
21
+
22
+ Bridges the new unified logging system with the existing TUI widget.
23
+ """
24
+
25
+ def __init__(self, log_widget: "ConversationLog"):
26
+ self.log = log_widget
27
+ self.formatter = UnifiedLogFormatter()
28
+ self._streaming_started = False
29
+
30
+ def emit(self, entry: LogEntry, config: LogConfig) -> None:
31
+ """Emit a log entry to the conversation log."""
32
+ self.formatter.config = config
33
+
34
+ # Route to appropriate method based on entry kind
35
+ handlers = {
36
+ "thinking": self._emit_thinking,
37
+ "tool_call": self._emit_tool_call,
38
+ "tool_result": self._emit_tool_result,
39
+ "tool_update": self._emit_tool_update,
40
+ "response_delta": self._emit_response_delta,
41
+ "response_final": self._emit_response_final,
42
+ "code_block": self._emit_code_block,
43
+ "info": self._emit_info,
44
+ "warning": self._emit_warning,
45
+ "error": self._emit_error,
46
+ "system": self._emit_system,
47
+ "user": self._emit_user,
48
+ "assistant": self._emit_assistant,
49
+ }
50
+
51
+ handler = handlers.get(entry.kind)
52
+ if handler:
53
+ handler(entry, config)
54
+
55
+ def _emit_thinking(self, entry: LogEntry, config: LogConfig) -> None:
56
+ """Emit thinking entry."""
57
+ if not config.show_thinking:
58
+ return
59
+
60
+ renderable = self.formatter.format(entry)
61
+ if renderable:
62
+ self.log.write(renderable)
63
+
64
+ def _emit_tool_call(self, entry: LogEntry, config: LogConfig) -> None:
65
+ """Emit tool call entry."""
66
+ renderable = self.formatter.format(entry)
67
+ if renderable:
68
+ self.log.write(renderable)
69
+
70
+ def _emit_tool_result(self, entry: LogEntry, config: LogConfig) -> None:
71
+ """Emit tool result entry."""
72
+ renderable = self.formatter.format(entry)
73
+ if renderable:
74
+ self.log.write(renderable)
75
+
76
+ def _emit_tool_update(self, entry: LogEntry, config: LogConfig) -> None:
77
+ """Emit tool update entry."""
78
+ renderable = self.formatter.format(entry)
79
+ if renderable:
80
+ self.log.write(renderable)
81
+
82
+ def _emit_response_delta(self, entry: LogEntry, config: LogConfig) -> None:
83
+ """Emit streaming response chunk."""
84
+ # For streaming, just write plain text
85
+ if entry.text:
86
+ from rich.text import Text
87
+
88
+ self.log.write(Text(entry.text))
89
+ self._streaming_started = True
90
+
91
+ def _emit_response_final(self, entry: LogEntry, config: LogConfig) -> None:
92
+ """Emit final complete response."""
93
+ # If we were streaming, the content is already displayed
94
+ # Just mark streaming as done
95
+ self._streaming_started = False
96
+
97
+ def _emit_code_block(self, entry: LogEntry, config: LogConfig) -> None:
98
+ """Emit code block with syntax highlighting."""
99
+ renderable = self.formatter.format(entry)
100
+ if renderable:
101
+ self.log.write(renderable)
102
+
103
+ def _emit_info(self, entry: LogEntry, config: LogConfig) -> None:
104
+ """Emit info message."""
105
+ self.log.add_info(entry.text)
106
+
107
+ def _emit_warning(self, entry: LogEntry, config: LogConfig) -> None:
108
+ """Emit warning message."""
109
+ from rich.text import Text
110
+
111
+ self.log.write(Text(f" ⚠️ {entry.text}", style="#f59e0b"))
112
+
113
+ def _emit_error(self, entry: LogEntry, config: LogConfig) -> None:
114
+ """Emit error message."""
115
+ self.log.add_error(entry.text)
116
+
117
+ def _emit_system(self, entry: LogEntry, config: LogConfig) -> None:
118
+ """Emit system message."""
119
+ self.log.add_system(entry.text)
120
+
121
+ def _emit_user(self, entry: LogEntry, config: LogConfig) -> None:
122
+ """Emit user message."""
123
+ self.log.add_user(entry.text)
124
+
125
+ def _emit_assistant(self, entry: LogEntry, config: LogConfig) -> None:
126
+ """Emit assistant message."""
127
+ self.log.add_agent(entry.text, entry.agent)
128
+
129
+
130
+ class BufferSink:
131
+ """
132
+ Sink that buffers entries for later processing.
133
+
134
+ Useful for testing or delayed rendering.
135
+ """
136
+
137
+ def __init__(self):
138
+ self.entries: list[LogEntry] = []
139
+
140
+ def emit(self, entry: LogEntry, config: LogConfig) -> None:
141
+ """Store entry in buffer."""
142
+ self.entries.append(entry)
143
+
144
+ def clear(self) -> None:
145
+ """Clear buffer."""
146
+ self.entries.clear()
147
+
148
+ def get_entries(self, kind: Optional[str] = None) -> list[LogEntry]:
149
+ """Get entries, optionally filtered by kind."""
150
+ if kind:
151
+ return [e for e in self.entries if e.kind == kind]
152
+ return self.entries.copy()
153
+
154
+
155
+ class CallbackSink:
156
+ """
157
+ Sink that calls a callback function for each entry.
158
+
159
+ Useful for custom handling or bridging to other systems.
160
+ """
161
+
162
+ def __init__(self, callback):
163
+ self.callback = callback
164
+ self.formatter = UnifiedLogFormatter()
165
+
166
+ def emit(self, entry: LogEntry, config: LogConfig) -> None:
167
+ """Call the callback with the entry."""
168
+ self.formatter.config = config
169
+ renderable = self.formatter.format(entry)
170
+ self.callback(entry, renderable)