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,815 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Thinking Display Widget - Model Reasoning Visualization.
|
|
3
|
+
|
|
4
|
+
Shows the model's thinking process in an elegant, expandable display:
|
|
5
|
+
- Streaming thinking text with typing effect
|
|
6
|
+
- Collapsible sections
|
|
7
|
+
- Thought categorization (planning, analyzing, deciding)
|
|
8
|
+
- Visual timeline of thoughts
|
|
9
|
+
- Support for extended thinking (Claude-style)
|
|
10
|
+
|
|
11
|
+
Provides transparency into how the model reasons about problems.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
from rich.console import RenderableType, Group
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.text import Text
|
|
25
|
+
from rich.box import ROUNDED
|
|
26
|
+
from textual.reactive import reactive
|
|
27
|
+
from textual.widgets import Static
|
|
28
|
+
from textual.containers import Container, Vertical
|
|
29
|
+
from textual.timer import Timer
|
|
30
|
+
from textual import events
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ThoughtType(Enum):
|
|
34
|
+
"""Type of thought/reasoning."""
|
|
35
|
+
|
|
36
|
+
PLANNING = "planning"
|
|
37
|
+
ANALYZING = "analyzing"
|
|
38
|
+
DECIDING = "deciding"
|
|
39
|
+
SEARCHING = "searching"
|
|
40
|
+
READING = "reading"
|
|
41
|
+
WRITING = "writing"
|
|
42
|
+
DEBUGGING = "debugging"
|
|
43
|
+
REFLECTING = "reflecting"
|
|
44
|
+
EXECUTING = "executing"
|
|
45
|
+
VERIFYING = "verifying"
|
|
46
|
+
TESTING = "testing"
|
|
47
|
+
REFACTORING = "refactoring"
|
|
48
|
+
GENERAL = "general"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Thought styling - colorful icons for visual clarity
|
|
52
|
+
THOUGHT_STYLES = {
|
|
53
|
+
ThoughtType.PLANNING: {"icon": "📋", "color": "#3b82f6", "label": "Planning"},
|
|
54
|
+
ThoughtType.ANALYZING: {"icon": "🔬", "color": "#8b5cf6", "label": "Analyzing"},
|
|
55
|
+
ThoughtType.DECIDING: {"icon": "🤔", "color": "#f59e0b", "label": "Deciding"},
|
|
56
|
+
ThoughtType.SEARCHING: {"icon": "🔍", "color": "#06b6d4", "label": "Searching"},
|
|
57
|
+
ThoughtType.READING: {"icon": "📖", "color": "#14b8a6", "label": "Reading"},
|
|
58
|
+
ThoughtType.WRITING: {"icon": "✏️", "color": "#22c55e", "label": "Writing"},
|
|
59
|
+
ThoughtType.DEBUGGING: {"icon": "🐛", "color": "#ef4444", "label": "Debugging"},
|
|
60
|
+
ThoughtType.REFLECTING: {"icon": "💭", "color": "#ec4899", "label": "Reflecting"},
|
|
61
|
+
ThoughtType.EXECUTING: {"icon": "⚡", "color": "#f97316", "label": "Executing"},
|
|
62
|
+
ThoughtType.VERIFYING: {"icon": "✅", "color": "#10b981", "label": "Verifying"},
|
|
63
|
+
ThoughtType.TESTING: {"icon": "🧪", "color": "#6366f1", "label": "Testing"},
|
|
64
|
+
ThoughtType.REFACTORING: {"icon": "🔧", "color": "#a855f7", "label": "Refactoring"},
|
|
65
|
+
ThoughtType.GENERAL: {"icon": "💡", "color": "#a1a1aa", "label": "Thinking"},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ThoughtChunk:
|
|
71
|
+
"""A chunk of thinking text."""
|
|
72
|
+
|
|
73
|
+
text: str
|
|
74
|
+
thought_type: ThoughtType = ThoughtType.GENERAL
|
|
75
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
76
|
+
is_streaming: bool = False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def classify_thought(text: str) -> ThoughtType:
|
|
80
|
+
"""
|
|
81
|
+
Classify a thought based on its content.
|
|
82
|
+
|
|
83
|
+
Uses keyword matching to determine the type of reasoning/activity
|
|
84
|
+
the agent is performing. Order matters - more specific matches first.
|
|
85
|
+
"""
|
|
86
|
+
text_lower = text.lower()
|
|
87
|
+
|
|
88
|
+
# Testing - check first as it's specific
|
|
89
|
+
if any(
|
|
90
|
+
w in text_lower
|
|
91
|
+
for w in ["test", "pytest", "unittest", "assertion", "expect", "should pass", "should fail"]
|
|
92
|
+
):
|
|
93
|
+
return ThoughtType.TESTING
|
|
94
|
+
|
|
95
|
+
# Verifying - checking if something works
|
|
96
|
+
if any(
|
|
97
|
+
w in text_lower
|
|
98
|
+
for w in ["verify", "confirm", "validate", "check if", "ensure", "make sure", "works"]
|
|
99
|
+
):
|
|
100
|
+
return ThoughtType.VERIFYING
|
|
101
|
+
|
|
102
|
+
# Executing - running commands
|
|
103
|
+
if any(
|
|
104
|
+
w in text_lower
|
|
105
|
+
for w in ["run", "execute", "running", "executing", "shell", "command", "npm", "pip"]
|
|
106
|
+
):
|
|
107
|
+
return ThoughtType.EXECUTING
|
|
108
|
+
|
|
109
|
+
# Refactoring - restructuring code
|
|
110
|
+
if any(
|
|
111
|
+
w in text_lower
|
|
112
|
+
for w in ["refactor", "restructure", "reorganize", "clean up", "simplify", "extract"]
|
|
113
|
+
):
|
|
114
|
+
return ThoughtType.REFACTORING
|
|
115
|
+
|
|
116
|
+
# Debugging - fixing issues
|
|
117
|
+
if any(
|
|
118
|
+
w in text_lower
|
|
119
|
+
for w in ["debug", "error", "fix", "issue", "problem", "bug", "traceback", "exception"]
|
|
120
|
+
):
|
|
121
|
+
return ThoughtType.DEBUGGING
|
|
122
|
+
|
|
123
|
+
# Planning - strategizing approach
|
|
124
|
+
if any(
|
|
125
|
+
w in text_lower
|
|
126
|
+
for w in ["plan", "step", "approach", "strategy", "first", "then", "next", "let me", "i'll"]
|
|
127
|
+
):
|
|
128
|
+
return ThoughtType.PLANNING
|
|
129
|
+
|
|
130
|
+
# Analyzing - understanding code/problem
|
|
131
|
+
if any(
|
|
132
|
+
w in text_lower
|
|
133
|
+
for w in ["analyze", "understand", "examine", "look at", "check", "inspect", "review"]
|
|
134
|
+
):
|
|
135
|
+
return ThoughtType.ANALYZING
|
|
136
|
+
|
|
137
|
+
# Deciding - making choices
|
|
138
|
+
if any(
|
|
139
|
+
w in text_lower
|
|
140
|
+
for w in ["decide", "choose", "option", "should i", "best way", "which", "either", "or"]
|
|
141
|
+
):
|
|
142
|
+
return ThoughtType.DECIDING
|
|
143
|
+
|
|
144
|
+
# Searching - finding files/code
|
|
145
|
+
if any(
|
|
146
|
+
w in text_lower
|
|
147
|
+
for w in ["search", "find", "look for", "grep", "locate", "where is", "looking for"]
|
|
148
|
+
):
|
|
149
|
+
return ThoughtType.SEARCHING
|
|
150
|
+
|
|
151
|
+
# Reading - examining content
|
|
152
|
+
if any(
|
|
153
|
+
w in text_lower
|
|
154
|
+
for w in ["read", "reading", "content", "see what", "open", "view", "contents of"]
|
|
155
|
+
):
|
|
156
|
+
return ThoughtType.READING
|
|
157
|
+
|
|
158
|
+
# Writing - creating/modifying code
|
|
159
|
+
if any(
|
|
160
|
+
w in text_lower
|
|
161
|
+
for w in ["write", "create", "add", "implement", "modify", "update", "change", "edit"]
|
|
162
|
+
):
|
|
163
|
+
return ThoughtType.WRITING
|
|
164
|
+
|
|
165
|
+
# Reflecting - meta-thinking
|
|
166
|
+
if any(
|
|
167
|
+
w in text_lower
|
|
168
|
+
for w in ["think", "consider", "hmm", "wait", "actually", "interesting", "notice"]
|
|
169
|
+
):
|
|
170
|
+
return ThoughtType.REFLECTING
|
|
171
|
+
|
|
172
|
+
return ThoughtType.GENERAL
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class ThinkingBubble(Static):
|
|
176
|
+
"""Single thinking bubble widget."""
|
|
177
|
+
|
|
178
|
+
DEFAULT_CSS = """
|
|
179
|
+
ThinkingBubble {
|
|
180
|
+
height: auto;
|
|
181
|
+
margin: 0 0 0 2;
|
|
182
|
+
padding: 0;
|
|
183
|
+
}
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def __init__(self, chunk: ThoughtChunk, **kwargs):
|
|
187
|
+
super().__init__(**kwargs)
|
|
188
|
+
self.chunk = chunk
|
|
189
|
+
|
|
190
|
+
def render(self) -> Text:
|
|
191
|
+
style = THOUGHT_STYLES.get(self.chunk.thought_type, THOUGHT_STYLES[ThoughtType.GENERAL])
|
|
192
|
+
|
|
193
|
+
result = Text()
|
|
194
|
+
result.append(f"{style['icon']} ", style=style["color"])
|
|
195
|
+
|
|
196
|
+
# Truncate long thoughts
|
|
197
|
+
text = self.chunk.text
|
|
198
|
+
if len(text) > 150:
|
|
199
|
+
text = text[:147] + "..."
|
|
200
|
+
|
|
201
|
+
result.append(text, style=f"italic #a1a1aa")
|
|
202
|
+
|
|
203
|
+
# Streaming indicator
|
|
204
|
+
if self.chunk.is_streaming:
|
|
205
|
+
result.append(" ●", style="bold #fbbf24")
|
|
206
|
+
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class ThinkingPanel(Container):
|
|
211
|
+
"""
|
|
212
|
+
Panel displaying model thinking/reasoning.
|
|
213
|
+
|
|
214
|
+
Features:
|
|
215
|
+
- Collapsible display
|
|
216
|
+
- Streaming support
|
|
217
|
+
- Thought categorization
|
|
218
|
+
- Summary when collapsed
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
DEFAULT_CSS = """
|
|
222
|
+
ThinkingPanel {
|
|
223
|
+
height: auto;
|
|
224
|
+
max-height: 30%;
|
|
225
|
+
border: solid #27272a;
|
|
226
|
+
border-left: tall #ec4899;
|
|
227
|
+
background: #0d0d0d;
|
|
228
|
+
padding: 0 1;
|
|
229
|
+
margin: 0 0 1 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
ThinkingPanel.collapsed {
|
|
233
|
+
max-height: 3;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
ThinkingPanel .thinking-header {
|
|
237
|
+
height: 1;
|
|
238
|
+
margin-bottom: 1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
ThinkingPanel .thinking-content {
|
|
242
|
+
height: auto;
|
|
243
|
+
max-height: 20;
|
|
244
|
+
overflow-y: auto;
|
|
245
|
+
}
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
collapsed: reactive[bool] = reactive(False)
|
|
249
|
+
is_streaming: reactive[bool] = reactive(False)
|
|
250
|
+
|
|
251
|
+
def __init__(self, **kwargs):
|
|
252
|
+
super().__init__(**kwargs)
|
|
253
|
+
self._chunks: List[ThoughtChunk] = []
|
|
254
|
+
self._current_text = ""
|
|
255
|
+
self._timer: Optional[Timer] = None
|
|
256
|
+
|
|
257
|
+
def on_mount(self) -> None:
|
|
258
|
+
"""Start animation timer."""
|
|
259
|
+
self._timer = self.set_interval(0.5, self._tick)
|
|
260
|
+
|
|
261
|
+
def _tick(self) -> None:
|
|
262
|
+
"""Animation tick."""
|
|
263
|
+
if self.is_streaming:
|
|
264
|
+
self._update_header()
|
|
265
|
+
|
|
266
|
+
def toggle(self) -> None:
|
|
267
|
+
"""Toggle collapsed state."""
|
|
268
|
+
self.collapsed = not self.collapsed
|
|
269
|
+
self.set_class(self.collapsed, "collapsed")
|
|
270
|
+
self._update_display()
|
|
271
|
+
|
|
272
|
+
def on_click(self, event: events.Click) -> None:
|
|
273
|
+
"""Toggle on click."""
|
|
274
|
+
self.toggle()
|
|
275
|
+
|
|
276
|
+
def start_streaming(self) -> None:
|
|
277
|
+
"""Start a new streaming thought."""
|
|
278
|
+
self.is_streaming = True
|
|
279
|
+
self._current_text = ""
|
|
280
|
+
self._update_header()
|
|
281
|
+
|
|
282
|
+
def append_chunk(self, text: str) -> None:
|
|
283
|
+
"""Append text to current streaming thought."""
|
|
284
|
+
self._current_text += text
|
|
285
|
+
self._update_display()
|
|
286
|
+
|
|
287
|
+
def complete_thought(self) -> None:
|
|
288
|
+
"""Complete the current streaming thought."""
|
|
289
|
+
if self._current_text:
|
|
290
|
+
thought_type = classify_thought(self._current_text)
|
|
291
|
+
chunk = ThoughtChunk(
|
|
292
|
+
text=self._current_text.strip(),
|
|
293
|
+
thought_type=thought_type,
|
|
294
|
+
is_streaming=False,
|
|
295
|
+
)
|
|
296
|
+
self._chunks.append(chunk)
|
|
297
|
+
|
|
298
|
+
self._current_text = ""
|
|
299
|
+
self.is_streaming = False
|
|
300
|
+
self._update_display()
|
|
301
|
+
|
|
302
|
+
def add_thought(self, text: str) -> None:
|
|
303
|
+
"""Add a complete thought."""
|
|
304
|
+
thought_type = classify_thought(text)
|
|
305
|
+
chunk = ThoughtChunk(
|
|
306
|
+
text=text.strip(),
|
|
307
|
+
thought_type=thought_type,
|
|
308
|
+
is_streaming=False,
|
|
309
|
+
)
|
|
310
|
+
self._chunks.append(chunk)
|
|
311
|
+
self._update_display()
|
|
312
|
+
|
|
313
|
+
def _update_header(self) -> None:
|
|
314
|
+
"""Update the header."""
|
|
315
|
+
try:
|
|
316
|
+
header = self.query_one(".thinking-header", Static)
|
|
317
|
+
except Exception:
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
text = Text()
|
|
321
|
+
text.append("💭 ", style="bold #ec4899")
|
|
322
|
+
text.append("Thinking", style="bold #e4e4e7")
|
|
323
|
+
|
|
324
|
+
if self.is_streaming:
|
|
325
|
+
text.append(" ● ", style="bold #fbbf24")
|
|
326
|
+
text.append("Streaming...", style="italic #fbbf24")
|
|
327
|
+
elif self._chunks:
|
|
328
|
+
text.append(f" ({len(self._chunks)} thoughts)", style="#6b7280")
|
|
329
|
+
|
|
330
|
+
# Collapse indicator
|
|
331
|
+
icon = "▶" if self.collapsed else "▼"
|
|
332
|
+
text.append(f" {icon}", style="#52525b")
|
|
333
|
+
|
|
334
|
+
header.update(text)
|
|
335
|
+
|
|
336
|
+
def _update_display(self) -> None:
|
|
337
|
+
"""Update the display."""
|
|
338
|
+
self._update_header()
|
|
339
|
+
|
|
340
|
+
if self.collapsed:
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
content = self.query_one(".thinking-content", Container)
|
|
345
|
+
except Exception:
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
# Clear existing bubbles
|
|
349
|
+
content.remove_children()
|
|
350
|
+
|
|
351
|
+
# Add thought bubbles
|
|
352
|
+
visible_chunks = self._chunks[-10:] # Show last 10
|
|
353
|
+
for chunk in visible_chunks:
|
|
354
|
+
bubble = ThinkingBubble(chunk)
|
|
355
|
+
content.mount(bubble)
|
|
356
|
+
|
|
357
|
+
# Add current streaming chunk
|
|
358
|
+
if self._current_text:
|
|
359
|
+
streaming_chunk = ThoughtChunk(
|
|
360
|
+
text=self._current_text,
|
|
361
|
+
thought_type=classify_thought(self._current_text),
|
|
362
|
+
is_streaming=True,
|
|
363
|
+
)
|
|
364
|
+
content.mount(ThinkingBubble(streaming_chunk))
|
|
365
|
+
|
|
366
|
+
def clear(self) -> None:
|
|
367
|
+
"""Clear all thoughts."""
|
|
368
|
+
self._chunks.clear()
|
|
369
|
+
self._current_text = ""
|
|
370
|
+
self.is_streaming = False
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
content = self.query_one(".thinking-content", Container)
|
|
374
|
+
content.remove_children()
|
|
375
|
+
except Exception:
|
|
376
|
+
pass
|
|
377
|
+
|
|
378
|
+
self._update_header()
|
|
379
|
+
|
|
380
|
+
def compose(self):
|
|
381
|
+
"""Compose the panel."""
|
|
382
|
+
yield Static("", classes="thinking-header")
|
|
383
|
+
with Container(classes="thinking-content"):
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class ExtendedThinkingPanel(Container):
|
|
388
|
+
"""
|
|
389
|
+
Extended thinking display for Claude-style extended thinking.
|
|
390
|
+
|
|
391
|
+
Shows the full thinking trace with:
|
|
392
|
+
- Collapsible major sections
|
|
393
|
+
- Search through thoughts
|
|
394
|
+
- Copy thinking text
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
DEFAULT_CSS = """
|
|
398
|
+
ExtendedThinkingPanel {
|
|
399
|
+
height: auto;
|
|
400
|
+
max-height: 50%;
|
|
401
|
+
border: double #a855f7;
|
|
402
|
+
background: #0d0d0d;
|
|
403
|
+
padding: 1;
|
|
404
|
+
margin: 0 0 1 0;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
ExtendedThinkingPanel .extended-header {
|
|
408
|
+
height: 2;
|
|
409
|
+
border-bottom: solid #27272a;
|
|
410
|
+
margin-bottom: 1;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
ExtendedThinkingPanel .extended-content {
|
|
414
|
+
height: auto;
|
|
415
|
+
max-height: 40;
|
|
416
|
+
overflow-y: auto;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
ExtendedThinkingPanel .extended-footer {
|
|
420
|
+
height: 1;
|
|
421
|
+
border-top: solid #27272a;
|
|
422
|
+
margin-top: 1;
|
|
423
|
+
}
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
collapsed: reactive[bool] = reactive(False)
|
|
427
|
+
|
|
428
|
+
def __init__(self, **kwargs):
|
|
429
|
+
super().__init__(**kwargs)
|
|
430
|
+
self._thinking_text = ""
|
|
431
|
+
self._token_count = 0
|
|
432
|
+
|
|
433
|
+
def set_thinking(self, text: str, token_count: int = 0) -> None:
|
|
434
|
+
"""Set the extended thinking text."""
|
|
435
|
+
self._thinking_text = text
|
|
436
|
+
self._token_count = token_count
|
|
437
|
+
self._update_display()
|
|
438
|
+
|
|
439
|
+
def append_thinking(self, text: str) -> None:
|
|
440
|
+
"""Append to thinking text."""
|
|
441
|
+
self._thinking_text += text
|
|
442
|
+
self._update_display()
|
|
443
|
+
|
|
444
|
+
def _update_display(self) -> None:
|
|
445
|
+
"""Update the display."""
|
|
446
|
+
try:
|
|
447
|
+
header = self.query_one(".extended-header", Static)
|
|
448
|
+
content = self.query_one(".extended-content", Static)
|
|
449
|
+
footer = self.query_one(".extended-footer", Static)
|
|
450
|
+
except Exception:
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
# Header
|
|
454
|
+
header_text = Text()
|
|
455
|
+
header_text.append("🧠 ", style="bold #a855f7")
|
|
456
|
+
header_text.append("Extended Thinking", style="bold #e4e4e7")
|
|
457
|
+
|
|
458
|
+
if self._token_count:
|
|
459
|
+
header_text.append(f" ({self._token_count} tokens)", style="#6b7280")
|
|
460
|
+
|
|
461
|
+
header.update(header_text)
|
|
462
|
+
|
|
463
|
+
# Content
|
|
464
|
+
if self.collapsed:
|
|
465
|
+
content_text = Text()
|
|
466
|
+
lines = self._thinking_text.splitlines()
|
|
467
|
+
if lines:
|
|
468
|
+
preview = lines[0][:80] + "..." if len(lines[0]) > 80 else lines[0]
|
|
469
|
+
content_text.append(preview, style="italic #a1a1aa")
|
|
470
|
+
if len(lines) > 1:
|
|
471
|
+
content_text.append(f"\n... ({len(lines)} lines)", style="#52525b")
|
|
472
|
+
content.update(content_text)
|
|
473
|
+
else:
|
|
474
|
+
content.update(Text(self._thinking_text, style="#a1a1aa"))
|
|
475
|
+
|
|
476
|
+
# Footer
|
|
477
|
+
footer_text = Text()
|
|
478
|
+
word_count = len(self._thinking_text.split())
|
|
479
|
+
footer_text.append(f"📊 {word_count} words", style="#6b7280")
|
|
480
|
+
footer_text.append(" │ ", style="#27272a")
|
|
481
|
+
footer_text.append("[Space] Toggle", style="#52525b")
|
|
482
|
+
footer.update(footer_text)
|
|
483
|
+
|
|
484
|
+
def toggle(self) -> None:
|
|
485
|
+
"""Toggle collapsed state."""
|
|
486
|
+
self.collapsed = not self.collapsed
|
|
487
|
+
self._update_display()
|
|
488
|
+
|
|
489
|
+
def on_key(self, event: events.Key) -> None:
|
|
490
|
+
"""Handle key events."""
|
|
491
|
+
if event.key == "space":
|
|
492
|
+
self.toggle()
|
|
493
|
+
event.prevent_default()
|
|
494
|
+
|
|
495
|
+
def clear(self) -> None:
|
|
496
|
+
"""Clear thinking."""
|
|
497
|
+
self._thinking_text = ""
|
|
498
|
+
self._token_count = 0
|
|
499
|
+
self._update_display()
|
|
500
|
+
|
|
501
|
+
def compose(self):
|
|
502
|
+
"""Compose the panel."""
|
|
503
|
+
yield Static("", classes="extended-header")
|
|
504
|
+
yield Static("", classes="extended-content")
|
|
505
|
+
yield Static("", classes="extended-footer")
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class ThinkingIndicator(Static):
|
|
509
|
+
"""Compact thinking indicator for status bar."""
|
|
510
|
+
|
|
511
|
+
DEFAULT_CSS = """
|
|
512
|
+
ThinkingIndicator {
|
|
513
|
+
width: auto;
|
|
514
|
+
height: 1;
|
|
515
|
+
padding: 0 1;
|
|
516
|
+
}
|
|
517
|
+
"""
|
|
518
|
+
|
|
519
|
+
thinking: reactive[bool] = reactive(False)
|
|
520
|
+
|
|
521
|
+
def __init__(self, **kwargs):
|
|
522
|
+
super().__init__(**kwargs)
|
|
523
|
+
self._frame = 0
|
|
524
|
+
self._thought_count = 0
|
|
525
|
+
|
|
526
|
+
def set_thinking(self, is_thinking: bool) -> None:
|
|
527
|
+
"""Set thinking state."""
|
|
528
|
+
self.thinking = is_thinking
|
|
529
|
+
self.refresh()
|
|
530
|
+
|
|
531
|
+
def set_count(self, count: int) -> None:
|
|
532
|
+
"""Set thought count."""
|
|
533
|
+
self._thought_count = count
|
|
534
|
+
self.refresh()
|
|
535
|
+
|
|
536
|
+
def animate(self) -> None:
|
|
537
|
+
"""Advance animation frame."""
|
|
538
|
+
self._frame += 1
|
|
539
|
+
if self.thinking:
|
|
540
|
+
self.refresh()
|
|
541
|
+
|
|
542
|
+
def render(self) -> Text:
|
|
543
|
+
text = Text()
|
|
544
|
+
|
|
545
|
+
if self.thinking:
|
|
546
|
+
# Animated thinking indicator
|
|
547
|
+
frames = ["💭", "💬", "💭", "💬"]
|
|
548
|
+
icon = frames[self._frame % len(frames)]
|
|
549
|
+
text.append(f"{icon} ", style="bold #ec4899")
|
|
550
|
+
text.append("Thinking...", style="italic #ec4899")
|
|
551
|
+
elif self._thought_count > 0:
|
|
552
|
+
text.append("💭 ", style="#6b7280")
|
|
553
|
+
text.append(f"{self._thought_count}", style="#a1a1aa")
|
|
554
|
+
else:
|
|
555
|
+
text.append("💭 -", style="#52525b")
|
|
556
|
+
|
|
557
|
+
return text
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class ThinkingSource(Enum):
|
|
561
|
+
"""Source of thinking/reasoning content."""
|
|
562
|
+
|
|
563
|
+
ACP = "acp" # ACP agent_thought_chunk
|
|
564
|
+
BYOK = "byok" # BYOK StreamChunk.thinking_content
|
|
565
|
+
LOCAL = "local" # Local models (through BYOK gateway)
|
|
566
|
+
OPEN_RESPONSES = "openresponses" # Open Responses reasoning.delta
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
@dataclass
|
|
570
|
+
class ThinkingStats:
|
|
571
|
+
"""Statistics for thinking/reasoning."""
|
|
572
|
+
|
|
573
|
+
source: ThinkingSource
|
|
574
|
+
token_count: int = 0
|
|
575
|
+
thought_count: int = 0
|
|
576
|
+
duration_ms: float = 0.0
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
class UnifiedThinkingManager:
|
|
580
|
+
"""
|
|
581
|
+
Routes thinking from any source to ThinkingPanel.
|
|
582
|
+
|
|
583
|
+
Provides a unified interface for handling thinking/reasoning content
|
|
584
|
+
from multiple connection modes:
|
|
585
|
+
- ACP: agent_thought_chunk events
|
|
586
|
+
- BYOK: StreamChunk.thinking_content
|
|
587
|
+
- Local: Route through BYOK gateway
|
|
588
|
+
- OpenResponses: response.reasoning.delta events
|
|
589
|
+
|
|
590
|
+
Usage:
|
|
591
|
+
panel = ThinkingPanel()
|
|
592
|
+
manager = UnifiedThinkingManager(panel)
|
|
593
|
+
|
|
594
|
+
# In ACP mode
|
|
595
|
+
await manager.handle_acp_thought("Planning approach...")
|
|
596
|
+
|
|
597
|
+
# In BYOK mode
|
|
598
|
+
await manager.handle_byok_chunk(chunk)
|
|
599
|
+
|
|
600
|
+
# In Open Responses mode
|
|
601
|
+
await manager.handle_openresponses_event(event)
|
|
602
|
+
"""
|
|
603
|
+
|
|
604
|
+
def __init__(
|
|
605
|
+
self,
|
|
606
|
+
panel: ThinkingPanel,
|
|
607
|
+
extended_panel: Optional[ExtendedThinkingPanel] = None,
|
|
608
|
+
indicator: Optional[ThinkingIndicator] = None,
|
|
609
|
+
):
|
|
610
|
+
self.panel = panel
|
|
611
|
+
self.extended_panel = extended_panel
|
|
612
|
+
self.indicator = indicator
|
|
613
|
+
self._current_source: Optional[ThinkingSource] = None
|
|
614
|
+
self._stats = ThinkingStats(source=ThinkingSource.BYOK)
|
|
615
|
+
self._start_time: Optional[float] = None
|
|
616
|
+
self._is_streaming = False
|
|
617
|
+
|
|
618
|
+
def start_session(self, source: ThinkingSource) -> None:
|
|
619
|
+
"""Start a new thinking session."""
|
|
620
|
+
self._current_source = source
|
|
621
|
+
self._stats = ThinkingStats(source=source)
|
|
622
|
+
self._start_time = (
|
|
623
|
+
asyncio.get_event_loop().time() if asyncio.get_event_loop().is_running() else None
|
|
624
|
+
)
|
|
625
|
+
self._is_streaming = False
|
|
626
|
+
self.panel.clear()
|
|
627
|
+
if self.extended_panel:
|
|
628
|
+
self.extended_panel.clear()
|
|
629
|
+
|
|
630
|
+
def end_session(self) -> ThinkingStats:
|
|
631
|
+
"""End the current thinking session and return stats."""
|
|
632
|
+
if self._start_time:
|
|
633
|
+
try:
|
|
634
|
+
loop = asyncio.get_event_loop()
|
|
635
|
+
if loop.is_running():
|
|
636
|
+
self._stats.duration_ms = (loop.time() - self._start_time) * 1000
|
|
637
|
+
except RuntimeError:
|
|
638
|
+
pass
|
|
639
|
+
|
|
640
|
+
if self._is_streaming:
|
|
641
|
+
self.panel.complete_thought()
|
|
642
|
+
self._is_streaming = False
|
|
643
|
+
|
|
644
|
+
if self.indicator:
|
|
645
|
+
self.indicator.set_thinking(False)
|
|
646
|
+
self.indicator.set_count(self._stats.thought_count)
|
|
647
|
+
|
|
648
|
+
return self._stats
|
|
649
|
+
|
|
650
|
+
async def handle_acp_thought(self, text: str) -> None:
|
|
651
|
+
"""
|
|
652
|
+
Handle ACP agent_thought_chunk.
|
|
653
|
+
|
|
654
|
+
ACP sends complete thought chunks, not streaming deltas.
|
|
655
|
+
"""
|
|
656
|
+
if not text:
|
|
657
|
+
return
|
|
658
|
+
|
|
659
|
+
self._current_source = ThinkingSource.ACP
|
|
660
|
+
self._stats.thought_count += 1
|
|
661
|
+
|
|
662
|
+
# ACP thoughts are complete - add directly
|
|
663
|
+
self.panel.add_thought(text)
|
|
664
|
+
|
|
665
|
+
if self.indicator:
|
|
666
|
+
self.indicator.set_thinking(True)
|
|
667
|
+
|
|
668
|
+
async def handle_byok_chunk(self, chunk: Any) -> None:
|
|
669
|
+
"""
|
|
670
|
+
Handle BYOK StreamChunk.thinking_content.
|
|
671
|
+
|
|
672
|
+
BYOK sends streaming deltas that need to be accumulated.
|
|
673
|
+
|
|
674
|
+
Args:
|
|
675
|
+
chunk: StreamChunk with optional thinking_content field
|
|
676
|
+
"""
|
|
677
|
+
thinking_content = getattr(chunk, "thinking_content", None)
|
|
678
|
+
if not thinking_content:
|
|
679
|
+
return
|
|
680
|
+
|
|
681
|
+
self._current_source = ThinkingSource.BYOK
|
|
682
|
+
|
|
683
|
+
# Start streaming if not already
|
|
684
|
+
if not self._is_streaming:
|
|
685
|
+
self.panel.start_streaming()
|
|
686
|
+
self._is_streaming = True
|
|
687
|
+
if self.indicator:
|
|
688
|
+
self.indicator.set_thinking(True)
|
|
689
|
+
|
|
690
|
+
# Append chunk to current streaming thought
|
|
691
|
+
self.panel.append_chunk(thinking_content)
|
|
692
|
+
|
|
693
|
+
# Also update extended panel if available
|
|
694
|
+
if self.extended_panel:
|
|
695
|
+
self.extended_panel.append_thinking(thinking_content)
|
|
696
|
+
|
|
697
|
+
async def handle_byok_response(self, response: Any) -> None:
|
|
698
|
+
"""
|
|
699
|
+
Handle complete BYOK GatewayResponse.thinking_content.
|
|
700
|
+
|
|
701
|
+
Called after non-streaming completion with full thinking.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
response: GatewayResponse with optional thinking_content field
|
|
705
|
+
"""
|
|
706
|
+
thinking_content = getattr(response, "thinking_content", None)
|
|
707
|
+
thinking_tokens = getattr(response, "thinking_tokens", None)
|
|
708
|
+
|
|
709
|
+
if not thinking_content:
|
|
710
|
+
return
|
|
711
|
+
|
|
712
|
+
self._current_source = ThinkingSource.BYOK
|
|
713
|
+
self._stats.thought_count += 1
|
|
714
|
+
|
|
715
|
+
if thinking_tokens:
|
|
716
|
+
self._stats.token_count = thinking_tokens
|
|
717
|
+
|
|
718
|
+
# Add complete thought
|
|
719
|
+
self.panel.add_thought(thinking_content)
|
|
720
|
+
|
|
721
|
+
# Update extended panel with full content
|
|
722
|
+
if self.extended_panel:
|
|
723
|
+
self.extended_panel.set_thinking(thinking_content, thinking_tokens or 0)
|
|
724
|
+
|
|
725
|
+
if self.indicator:
|
|
726
|
+
self.indicator.set_count(self._stats.thought_count)
|
|
727
|
+
|
|
728
|
+
async def handle_openresponses_event(self, event: Dict[str, Any]) -> None:
|
|
729
|
+
"""
|
|
730
|
+
Handle Open Responses streaming event.
|
|
731
|
+
|
|
732
|
+
Open Responses sends multiple event types:
|
|
733
|
+
- response.reasoning.delta: Reasoning text delta
|
|
734
|
+
- response.reasoning.done: Reasoning complete
|
|
735
|
+
- response.output_text.delta: Regular output delta
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
event: Open Responses streaming event dict
|
|
739
|
+
"""
|
|
740
|
+
event_type = event.get("type", "")
|
|
741
|
+
|
|
742
|
+
if event_type == "response.reasoning.delta":
|
|
743
|
+
delta = event.get("delta", "")
|
|
744
|
+
if delta:
|
|
745
|
+
self._current_source = ThinkingSource.OPEN_RESPONSES
|
|
746
|
+
|
|
747
|
+
if not self._is_streaming:
|
|
748
|
+
self.panel.start_streaming()
|
|
749
|
+
self._is_streaming = True
|
|
750
|
+
if self.indicator:
|
|
751
|
+
self.indicator.set_thinking(True)
|
|
752
|
+
|
|
753
|
+
self.panel.append_chunk(delta)
|
|
754
|
+
|
|
755
|
+
if self.extended_panel:
|
|
756
|
+
self.extended_panel.append_thinking(delta)
|
|
757
|
+
|
|
758
|
+
elif event_type == "response.reasoning.done":
|
|
759
|
+
# Reasoning complete - finalize
|
|
760
|
+
if self._is_streaming:
|
|
761
|
+
self.panel.complete_thought()
|
|
762
|
+
self._is_streaming = False
|
|
763
|
+
self._stats.thought_count += 1
|
|
764
|
+
|
|
765
|
+
# Extract token count if available
|
|
766
|
+
usage = event.get("usage", {})
|
|
767
|
+
reasoning_tokens = usage.get("reasoning_tokens", 0)
|
|
768
|
+
if reasoning_tokens:
|
|
769
|
+
self._stats.token_count = reasoning_tokens
|
|
770
|
+
|
|
771
|
+
if self.indicator:
|
|
772
|
+
self.indicator.set_thinking(False)
|
|
773
|
+
self.indicator.set_count(self._stats.thought_count)
|
|
774
|
+
|
|
775
|
+
async def handle_local_thought(self, text: str) -> None:
|
|
776
|
+
"""
|
|
777
|
+
Handle Local model thinking (routed through BYOK).
|
|
778
|
+
|
|
779
|
+
Local models may provide thinking through various mechanisms
|
|
780
|
+
depending on the model and server.
|
|
781
|
+
"""
|
|
782
|
+
if not text:
|
|
783
|
+
return
|
|
784
|
+
|
|
785
|
+
self._current_source = ThinkingSource.LOCAL
|
|
786
|
+
self._stats.thought_count += 1
|
|
787
|
+
|
|
788
|
+
self.panel.add_thought(text)
|
|
789
|
+
|
|
790
|
+
if self.indicator:
|
|
791
|
+
self.indicator.set_thinking(True)
|
|
792
|
+
|
|
793
|
+
def complete_streaming(self) -> None:
|
|
794
|
+
"""Complete any in-progress streaming thought."""
|
|
795
|
+
if self._is_streaming:
|
|
796
|
+
self.panel.complete_thought()
|
|
797
|
+
self._is_streaming = False
|
|
798
|
+
self._stats.thought_count += 1
|
|
799
|
+
|
|
800
|
+
if self.indicator:
|
|
801
|
+
self.indicator.set_thinking(False)
|
|
802
|
+
|
|
803
|
+
def get_stats(self) -> ThinkingStats:
|
|
804
|
+
"""Get current thinking statistics."""
|
|
805
|
+
return self._stats
|
|
806
|
+
|
|
807
|
+
@property
|
|
808
|
+
def current_source(self) -> Optional[ThinkingSource]:
|
|
809
|
+
"""Get the current thinking source."""
|
|
810
|
+
return self._current_source
|
|
811
|
+
|
|
812
|
+
@property
|
|
813
|
+
def is_streaming(self) -> bool:
|
|
814
|
+
"""Check if currently streaming thinking content."""
|
|
815
|
+
return self._is_streaming
|