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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- 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)
|