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
superqode/stream_view.py
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SuperQode Stream View - Real-time Agent Output Display
|
|
3
|
+
|
|
4
|
+
Colorful widgets for displaying streaming agent output including:
|
|
5
|
+
- Message chunks (agent responses)
|
|
6
|
+
- Thinking/reasoning
|
|
7
|
+
- Tool calls with status
|
|
8
|
+
- Plans with task tracking
|
|
9
|
+
- Permission requests
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
17
|
+
from time import monotonic
|
|
18
|
+
|
|
19
|
+
from textual.app import ComposeResult
|
|
20
|
+
from textual.containers import Container, Vertical, Horizontal
|
|
21
|
+
from textual.widgets import Static, RichLog, Button
|
|
22
|
+
from textual.reactive import reactive
|
|
23
|
+
from textual.message import Message
|
|
24
|
+
|
|
25
|
+
from rich.text import Text
|
|
26
|
+
from rich.panel import Panel
|
|
27
|
+
from rich.box import ROUNDED
|
|
28
|
+
from rich.syntax import Syntax
|
|
29
|
+
|
|
30
|
+
from superqode.agent_stream import (
|
|
31
|
+
StreamEvent,
|
|
32
|
+
StreamEventType,
|
|
33
|
+
StreamMessage,
|
|
34
|
+
StreamThought,
|
|
35
|
+
StreamToolCall,
|
|
36
|
+
StreamPlan,
|
|
37
|
+
StreamPermission,
|
|
38
|
+
PlanTask,
|
|
39
|
+
ToolKind,
|
|
40
|
+
ToolStatus,
|
|
41
|
+
TaskStatus,
|
|
42
|
+
TaskPriority,
|
|
43
|
+
get_tool_icon,
|
|
44
|
+
get_status_icon,
|
|
45
|
+
get_status_color,
|
|
46
|
+
get_task_icon,
|
|
47
|
+
get_task_color,
|
|
48
|
+
STREAM_COLORS,
|
|
49
|
+
STREAM_ICONS,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ============================================================================
|
|
54
|
+
# THEME
|
|
55
|
+
# ============================================================================
|
|
56
|
+
|
|
57
|
+
THEME = {
|
|
58
|
+
"bg": "#0a0a0a",
|
|
59
|
+
"surface": "#111111",
|
|
60
|
+
"border": "#2a2a2a",
|
|
61
|
+
"purple": "#a855f7",
|
|
62
|
+
"pink": "#ec4899",
|
|
63
|
+
"orange": "#f97316",
|
|
64
|
+
"cyan": "#06b6d4",
|
|
65
|
+
"green": "#22c55e",
|
|
66
|
+
"red": "#ef4444",
|
|
67
|
+
"yellow": "#fbbf24",
|
|
68
|
+
"text": "#e4e4e7",
|
|
69
|
+
"muted": "#71717a",
|
|
70
|
+
"dim": "#52525b",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ============================================================================
|
|
75
|
+
# STREAMING MESSAGE WIDGET
|
|
76
|
+
# ============================================================================
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class StreamingMessage(Static):
|
|
80
|
+
"""Widget that displays streaming text with typing effect."""
|
|
81
|
+
|
|
82
|
+
text_content = reactive("")
|
|
83
|
+
is_complete = reactive(False)
|
|
84
|
+
|
|
85
|
+
def __init__(self, agent_name: str = "Agent", **kwargs):
|
|
86
|
+
super().__init__(**kwargs)
|
|
87
|
+
self.agent_name = agent_name
|
|
88
|
+
self._buffer = ""
|
|
89
|
+
|
|
90
|
+
def append_text(self, text: str):
|
|
91
|
+
"""Append text to the message."""
|
|
92
|
+
self._buffer += text
|
|
93
|
+
self.text_content = self._buffer
|
|
94
|
+
|
|
95
|
+
def mark_complete(self):
|
|
96
|
+
"""Mark the message as complete."""
|
|
97
|
+
self.is_complete = True
|
|
98
|
+
|
|
99
|
+
def render(self) -> Text:
|
|
100
|
+
result = Text()
|
|
101
|
+
|
|
102
|
+
# Header with agent name
|
|
103
|
+
color = STREAM_COLORS["message"]
|
|
104
|
+
result.append(f"💬 {self.agent_name}", style=f"bold {color}")
|
|
105
|
+
|
|
106
|
+
if not self.is_complete:
|
|
107
|
+
result.append(" ●", style=f"bold {STREAM_COLORS['progress']}")
|
|
108
|
+
|
|
109
|
+
result.append("\n", style="")
|
|
110
|
+
|
|
111
|
+
# Message content
|
|
112
|
+
if self.text_content:
|
|
113
|
+
result.append(self.text_content, style=THEME["text"])
|
|
114
|
+
else:
|
|
115
|
+
result.append("...", style=f"italic {THEME['muted']}")
|
|
116
|
+
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ThinkingBubble(Static):
|
|
121
|
+
"""Widget that displays agent's thinking process."""
|
|
122
|
+
|
|
123
|
+
thought = reactive("")
|
|
124
|
+
|
|
125
|
+
def __init__(self, **kwargs):
|
|
126
|
+
super().__init__(**kwargs)
|
|
127
|
+
self._thoughts: List[str] = []
|
|
128
|
+
|
|
129
|
+
def add_thought(self, text: str):
|
|
130
|
+
"""Add a thought."""
|
|
131
|
+
self._thoughts.append(text)
|
|
132
|
+
self.thought = text
|
|
133
|
+
|
|
134
|
+
def render(self) -> Text:
|
|
135
|
+
result = Text()
|
|
136
|
+
|
|
137
|
+
color = STREAM_COLORS["thought"]
|
|
138
|
+
result.append(f"💭 ", style=f"bold {color}")
|
|
139
|
+
result.append("Thinking", style=f"italic {color}")
|
|
140
|
+
result.append("\n", style="")
|
|
141
|
+
|
|
142
|
+
# Show last few thoughts
|
|
143
|
+
for thought in self._thoughts[-3:]:
|
|
144
|
+
thought_short = thought[:100] + "..." if len(thought) > 100 else thought
|
|
145
|
+
result.append(f" • {thought_short}\n", style=f"italic {THEME['muted']}")
|
|
146
|
+
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ============================================================================
|
|
151
|
+
# TOOL CALL WIDGET
|
|
152
|
+
# ============================================================================
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ToolCallWidget(Static):
|
|
156
|
+
"""Widget displaying a tool call with status and content."""
|
|
157
|
+
|
|
158
|
+
status = reactive(ToolStatus.PENDING)
|
|
159
|
+
|
|
160
|
+
def __init__(self, tool_call: StreamToolCall, **kwargs):
|
|
161
|
+
super().__init__(**kwargs)
|
|
162
|
+
self.tool_call = tool_call
|
|
163
|
+
|
|
164
|
+
def update_tool(self, tool_call: StreamToolCall):
|
|
165
|
+
"""Update the tool call data."""
|
|
166
|
+
self.tool_call = tool_call
|
|
167
|
+
self.status = tool_call.status
|
|
168
|
+
|
|
169
|
+
def render(self) -> Text:
|
|
170
|
+
result = Text()
|
|
171
|
+
tc = self.tool_call
|
|
172
|
+
|
|
173
|
+
# Status icon and color
|
|
174
|
+
status_icon = get_status_icon(tc.status)
|
|
175
|
+
status_color = get_status_color(tc.status)
|
|
176
|
+
tool_icon = get_tool_icon(tc.kind)
|
|
177
|
+
|
|
178
|
+
# Header line
|
|
179
|
+
result.append(f"{status_icon} ", style=status_color)
|
|
180
|
+
result.append(f"{tool_icon} ", style=STREAM_COLORS["tool"])
|
|
181
|
+
result.append(tc.title, style=f"bold {status_color}")
|
|
182
|
+
|
|
183
|
+
# Location info
|
|
184
|
+
if tc.locations:
|
|
185
|
+
loc = tc.locations[0]
|
|
186
|
+
path = loc.get("path", "")
|
|
187
|
+
line = loc.get("line")
|
|
188
|
+
if path:
|
|
189
|
+
result.append(f" 📍 {path}", style=THEME["muted"])
|
|
190
|
+
if line:
|
|
191
|
+
result.append(f":{line}", style=THEME["dim"])
|
|
192
|
+
|
|
193
|
+
result.append("\n", style="")
|
|
194
|
+
|
|
195
|
+
# Content preview
|
|
196
|
+
for content in tc.content[:2]: # Show first 2 content items
|
|
197
|
+
if content.type == "diff":
|
|
198
|
+
data = content.data
|
|
199
|
+
path = data.get("path", "")
|
|
200
|
+
old_text = data.get("oldText", "")
|
|
201
|
+
new_text = data.get("newText", "")
|
|
202
|
+
|
|
203
|
+
result.append(f" 📄 {path}\n", style=THEME["cyan"])
|
|
204
|
+
|
|
205
|
+
# Show diff preview
|
|
206
|
+
if old_text:
|
|
207
|
+
old_lines = old_text.split("\n")[:3]
|
|
208
|
+
for line in old_lines:
|
|
209
|
+
result.append(f" - {line[:60]}\n", style=f"on #2d1f1f {THEME['red']}")
|
|
210
|
+
if new_text:
|
|
211
|
+
new_lines = new_text.split("\n")[:3]
|
|
212
|
+
for line in new_lines:
|
|
213
|
+
result.append(f" + {line[:60]}\n", style=f"on #1f2d1f {THEME['green']}")
|
|
214
|
+
|
|
215
|
+
elif content.type == "terminal":
|
|
216
|
+
terminal_id = content.data.get("terminalId", "")
|
|
217
|
+
result.append(f" 🖥️ Terminal: {terminal_id}\n", style=THEME["muted"])
|
|
218
|
+
|
|
219
|
+
elif content.type == "content":
|
|
220
|
+
text = content.data.get("text", "")
|
|
221
|
+
if text:
|
|
222
|
+
preview = text[:100] + "..." if len(text) > 100 else text
|
|
223
|
+
result.append(f" {preview}\n", style=THEME["dim"])
|
|
224
|
+
|
|
225
|
+
return result
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ============================================================================
|
|
229
|
+
# PLAN WIDGET
|
|
230
|
+
# ============================================================================
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class PlanWidget(Static):
|
|
234
|
+
"""Widget displaying agent's plan with task status."""
|
|
235
|
+
|
|
236
|
+
def __init__(self, plan: StreamPlan, **kwargs):
|
|
237
|
+
super().__init__(**kwargs)
|
|
238
|
+
self.plan = plan
|
|
239
|
+
|
|
240
|
+
def update_plan(self, plan: StreamPlan):
|
|
241
|
+
"""Update the plan."""
|
|
242
|
+
self.plan = plan
|
|
243
|
+
self.refresh()
|
|
244
|
+
|
|
245
|
+
def render(self) -> Text:
|
|
246
|
+
result = Text()
|
|
247
|
+
|
|
248
|
+
# Header
|
|
249
|
+
result.append(f"📋 ", style=f"bold {STREAM_COLORS['plan']}")
|
|
250
|
+
result.append("Plan", style=f"bold {STREAM_COLORS['plan']}")
|
|
251
|
+
|
|
252
|
+
# Progress
|
|
253
|
+
completed = sum(1 for t in self.plan.tasks if t.status == TaskStatus.COMPLETED)
|
|
254
|
+
total = len(self.plan.tasks)
|
|
255
|
+
if total > 0:
|
|
256
|
+
result.append(f" ({completed}/{total})", style=THEME["muted"])
|
|
257
|
+
|
|
258
|
+
result.append("\n", style="")
|
|
259
|
+
|
|
260
|
+
# Tasks
|
|
261
|
+
for i, task in enumerate(self.plan.tasks, 1):
|
|
262
|
+
icon = get_task_icon(task.status)
|
|
263
|
+
color = get_task_color(task.status)
|
|
264
|
+
|
|
265
|
+
# Priority indicator
|
|
266
|
+
priority_icon = ""
|
|
267
|
+
if task.priority == TaskPriority.HIGH:
|
|
268
|
+
priority_icon = "🔴 "
|
|
269
|
+
elif task.priority == TaskPriority.LOW:
|
|
270
|
+
priority_icon = "🔵 "
|
|
271
|
+
|
|
272
|
+
result.append(f" {icon} ", style=color)
|
|
273
|
+
result.append(f"{priority_icon}", style="")
|
|
274
|
+
result.append(f"{i}. ", style=THEME["muted"])
|
|
275
|
+
|
|
276
|
+
# Strike through completed tasks
|
|
277
|
+
if task.status == TaskStatus.COMPLETED:
|
|
278
|
+
result.append(task.content, style=f"strike {THEME['dim']}")
|
|
279
|
+
else:
|
|
280
|
+
result.append(
|
|
281
|
+
task.content,
|
|
282
|
+
style=color if task.status == TaskStatus.IN_PROGRESS else THEME["text"],
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
result.append("\n", style="")
|
|
286
|
+
|
|
287
|
+
return result
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ============================================================================
|
|
291
|
+
# PERMISSION REQUEST WIDGET
|
|
292
|
+
# ============================================================================
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class PermissionRequest(Message):
|
|
296
|
+
"""Message sent when user responds to permission request."""
|
|
297
|
+
|
|
298
|
+
def __init__(self, option_id: str):
|
|
299
|
+
self.option_id = option_id
|
|
300
|
+
super().__init__()
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class PermissionWidget(Static):
|
|
304
|
+
"""Widget for permission requests with action buttons."""
|
|
305
|
+
|
|
306
|
+
DEFAULT_CSS = """
|
|
307
|
+
PermissionWidget {
|
|
308
|
+
height: auto;
|
|
309
|
+
padding: 1;
|
|
310
|
+
background: #1a1a1a;
|
|
311
|
+
border: round #f97316;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
PermissionWidget .permission-buttons {
|
|
315
|
+
height: auto;
|
|
316
|
+
margin-top: 1;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
PermissionWidget Button {
|
|
320
|
+
margin-right: 1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
PermissionWidget Button.allow {
|
|
324
|
+
background: #22c55e;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
PermissionWidget Button.reject {
|
|
328
|
+
background: #ef4444;
|
|
329
|
+
}
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
def __init__(self, permission: StreamPermission, **kwargs):
|
|
333
|
+
super().__init__(**kwargs)
|
|
334
|
+
self.permission = permission
|
|
335
|
+
|
|
336
|
+
def compose(self) -> ComposeResult:
|
|
337
|
+
yield Static(self._render_header(), id="permission-header")
|
|
338
|
+
yield Static(self._render_tool_info(), id="permission-tool")
|
|
339
|
+
|
|
340
|
+
with Horizontal(classes="permission-buttons"):
|
|
341
|
+
for option in self.permission.options:
|
|
342
|
+
btn_class = "allow" if "allow" in option.kind else "reject"
|
|
343
|
+
yield Button(
|
|
344
|
+
option.name,
|
|
345
|
+
id=f"perm-{option.option_id}",
|
|
346
|
+
classes=btn_class,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def _render_header(self) -> Text:
|
|
350
|
+
result = Text()
|
|
351
|
+
result.append("🔐 ", style=f"bold {STREAM_COLORS['warning']}")
|
|
352
|
+
result.append("Permission Required", style=f"bold {STREAM_COLORS['warning']}")
|
|
353
|
+
return result
|
|
354
|
+
|
|
355
|
+
def _render_tool_info(self) -> Text:
|
|
356
|
+
result = Text()
|
|
357
|
+
tc = self.permission.tool_call
|
|
358
|
+
|
|
359
|
+
tool_icon = get_tool_icon(tc.kind)
|
|
360
|
+
result.append(f"\n{tool_icon} ", style=STREAM_COLORS["tool"])
|
|
361
|
+
result.append(tc.title, style=f"bold {THEME['text']}")
|
|
362
|
+
|
|
363
|
+
if tc.locations:
|
|
364
|
+
loc = tc.locations[0]
|
|
365
|
+
path = loc.get("path", "")
|
|
366
|
+
if path:
|
|
367
|
+
result.append(f"\n📍 {path}", style=THEME["muted"])
|
|
368
|
+
|
|
369
|
+
return result
|
|
370
|
+
|
|
371
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
372
|
+
"""Handle button press."""
|
|
373
|
+
button_id = event.button.id or ""
|
|
374
|
+
if button_id.startswith("perm-"):
|
|
375
|
+
option_id = button_id[5:] # Remove "perm-" prefix
|
|
376
|
+
|
|
377
|
+
# Resolve the future
|
|
378
|
+
if self.permission.result_future and not self.permission.result_future.done():
|
|
379
|
+
self.permission.result_future.set_result(option_id)
|
|
380
|
+
|
|
381
|
+
# Post message
|
|
382
|
+
self.post_message(PermissionRequest(option_id))
|
|
383
|
+
|
|
384
|
+
# Remove widget
|
|
385
|
+
self.remove()
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# ============================================================================
|
|
389
|
+
# STREAM VIEW CONTAINER
|
|
390
|
+
# ============================================================================
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class StreamView(Container):
|
|
394
|
+
"""
|
|
395
|
+
Container for displaying streaming agent output.
|
|
396
|
+
|
|
397
|
+
Manages multiple streaming widgets and updates them in real-time.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
DEFAULT_CSS = """
|
|
401
|
+
StreamView {
|
|
402
|
+
height: auto;
|
|
403
|
+
padding: 0 1;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
StreamView .stream-message {
|
|
407
|
+
margin-bottom: 1;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
StreamView .stream-thinking {
|
|
411
|
+
margin-bottom: 1;
|
|
412
|
+
padding: 0 1;
|
|
413
|
+
background: #1a1a1a;
|
|
414
|
+
border-left: tall #ec4899;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
StreamView .stream-tool {
|
|
418
|
+
margin-bottom: 1;
|
|
419
|
+
padding: 0 1;
|
|
420
|
+
background: #1a1a1a;
|
|
421
|
+
border-left: tall #f97316;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
StreamView .stream-plan {
|
|
425
|
+
margin-bottom: 1;
|
|
426
|
+
padding: 0 1;
|
|
427
|
+
background: #1a1a1a;
|
|
428
|
+
border-left: tall #06b6d4;
|
|
429
|
+
}
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
def __init__(self, agent_name: str = "Agent", **kwargs):
|
|
433
|
+
super().__init__(**kwargs)
|
|
434
|
+
self.agent_name = agent_name
|
|
435
|
+
self._current_message: Optional[StreamingMessage] = None
|
|
436
|
+
self._thinking: Optional[ThinkingBubble] = None
|
|
437
|
+
self._plan: Optional[PlanWidget] = None
|
|
438
|
+
self._tool_widgets: Dict[str, ToolCallWidget] = {}
|
|
439
|
+
|
|
440
|
+
def handle_event(self, event: StreamEvent):
|
|
441
|
+
"""Handle a streaming event."""
|
|
442
|
+
if event.event_type == StreamEventType.MESSAGE_CHUNK:
|
|
443
|
+
self._handle_message(event.data)
|
|
444
|
+
elif event.event_type == StreamEventType.THOUGHT_CHUNK:
|
|
445
|
+
self._handle_thought(event.data)
|
|
446
|
+
elif event.event_type == StreamEventType.TOOL_CALL:
|
|
447
|
+
self._handle_tool_call(event.data)
|
|
448
|
+
elif event.event_type == StreamEventType.TOOL_UPDATE:
|
|
449
|
+
self._handle_tool_update(event.data)
|
|
450
|
+
elif event.event_type == StreamEventType.PLAN:
|
|
451
|
+
self._handle_plan(event.data)
|
|
452
|
+
elif event.event_type == StreamEventType.PERMISSION:
|
|
453
|
+
self._handle_permission(event.data)
|
|
454
|
+
elif event.event_type == StreamEventType.ERROR:
|
|
455
|
+
self._handle_error(event.data)
|
|
456
|
+
elif event.event_type == StreamEventType.COMPLETE:
|
|
457
|
+
self._handle_complete()
|
|
458
|
+
|
|
459
|
+
def _handle_message(self, msg: StreamMessage):
|
|
460
|
+
"""Handle message chunk."""
|
|
461
|
+
if msg.is_complete:
|
|
462
|
+
if self._current_message:
|
|
463
|
+
self._current_message.mark_complete()
|
|
464
|
+
self._current_message = None
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
if not self._current_message:
|
|
468
|
+
self._current_message = StreamingMessage(
|
|
469
|
+
agent_name=self.agent_name, classes="stream-message"
|
|
470
|
+
)
|
|
471
|
+
self.mount(self._current_message)
|
|
472
|
+
|
|
473
|
+
self._current_message.append_text(msg.text)
|
|
474
|
+
|
|
475
|
+
def _handle_thought(self, thought: StreamThought):
|
|
476
|
+
"""Handle thought chunk."""
|
|
477
|
+
if not self._thinking:
|
|
478
|
+
self._thinking = ThinkingBubble(classes="stream-thinking")
|
|
479
|
+
self.mount(self._thinking)
|
|
480
|
+
|
|
481
|
+
self._thinking.add_thought(thought.text)
|
|
482
|
+
|
|
483
|
+
def _handle_tool_call(self, tool_call: StreamToolCall):
|
|
484
|
+
"""Handle new tool call."""
|
|
485
|
+
widget = ToolCallWidget(tool_call, classes="stream-tool")
|
|
486
|
+
self._tool_widgets[tool_call.tool_id] = widget
|
|
487
|
+
self.mount(widget)
|
|
488
|
+
|
|
489
|
+
def _handle_tool_update(self, tool_call: StreamToolCall):
|
|
490
|
+
"""Handle tool call update."""
|
|
491
|
+
if tool_call.tool_id in self._tool_widgets:
|
|
492
|
+
self._tool_widgets[tool_call.tool_id].update_tool(tool_call)
|
|
493
|
+
|
|
494
|
+
def _handle_plan(self, plan: StreamPlan):
|
|
495
|
+
"""Handle plan update."""
|
|
496
|
+
if not self._plan:
|
|
497
|
+
self._plan = PlanWidget(plan, classes="stream-plan")
|
|
498
|
+
self.mount(self._plan)
|
|
499
|
+
else:
|
|
500
|
+
self._plan.update_plan(plan)
|
|
501
|
+
|
|
502
|
+
def _handle_permission(self, permission: StreamPermission):
|
|
503
|
+
"""Handle permission request."""
|
|
504
|
+
widget = PermissionWidget(permission)
|
|
505
|
+
self.mount(widget)
|
|
506
|
+
|
|
507
|
+
def _handle_error(self, error: str):
|
|
508
|
+
"""Handle error."""
|
|
509
|
+
error_widget = Static(
|
|
510
|
+
Text(f"❌ {error}", style=STREAM_COLORS["error"]), classes="stream-error"
|
|
511
|
+
)
|
|
512
|
+
self.mount(error_widget)
|
|
513
|
+
|
|
514
|
+
def _handle_complete(self):
|
|
515
|
+
"""Handle completion."""
|
|
516
|
+
# Clear thinking bubble
|
|
517
|
+
if self._thinking:
|
|
518
|
+
self._thinking.remove()
|
|
519
|
+
self._thinking = None
|
|
520
|
+
|
|
521
|
+
# Mark message complete
|
|
522
|
+
if self._current_message:
|
|
523
|
+
self._current_message.mark_complete()
|
|
524
|
+
self._current_message = None
|
|
525
|
+
|
|
526
|
+
def clear(self):
|
|
527
|
+
"""Clear all streaming content."""
|
|
528
|
+
self._current_message = None
|
|
529
|
+
self._thinking = None
|
|
530
|
+
self._plan = None
|
|
531
|
+
self._tool_widgets.clear()
|
|
532
|
+
self.remove_children()
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# ============================================================================
|
|
536
|
+
# STREAMING STATUS BAR
|
|
537
|
+
# ============================================================================
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class StreamStatusBar(Static):
|
|
541
|
+
"""Status bar showing streaming progress with colorful indicators."""
|
|
542
|
+
|
|
543
|
+
DEFAULT_CSS = """
|
|
544
|
+
StreamStatusBar {
|
|
545
|
+
height: 1;
|
|
546
|
+
background: #111111;
|
|
547
|
+
padding: 0 1;
|
|
548
|
+
}
|
|
549
|
+
"""
|
|
550
|
+
|
|
551
|
+
status = reactive("idle")
|
|
552
|
+
agent_name = reactive("Agent")
|
|
553
|
+
tool_count = reactive(0)
|
|
554
|
+
message_length = reactive(0)
|
|
555
|
+
|
|
556
|
+
def render(self) -> Text:
|
|
557
|
+
result = Text()
|
|
558
|
+
|
|
559
|
+
# Agent indicator
|
|
560
|
+
result.append(f"🤖 {self.agent_name}", style=f"bold {STREAM_COLORS['message']}")
|
|
561
|
+
result.append(" │ ", style=THEME["dim"])
|
|
562
|
+
|
|
563
|
+
# Status with color
|
|
564
|
+
status_styles = {
|
|
565
|
+
"idle": ("○", THEME["muted"]),
|
|
566
|
+
"connecting": ("◌", STREAM_COLORS["warning"]),
|
|
567
|
+
"streaming": ("●", STREAM_COLORS["success"]),
|
|
568
|
+
"thinking": ("◐", STREAM_COLORS["thought"]),
|
|
569
|
+
"tool": ("◑", STREAM_COLORS["tool"]),
|
|
570
|
+
"complete": ("✓", STREAM_COLORS["success"]),
|
|
571
|
+
"error": ("✗", STREAM_COLORS["error"]),
|
|
572
|
+
}
|
|
573
|
+
icon, color = status_styles.get(self.status, ("○", THEME["muted"]))
|
|
574
|
+
result.append(f"{icon} ", style=color)
|
|
575
|
+
result.append(self.status.title(), style=color)
|
|
576
|
+
|
|
577
|
+
# Stats
|
|
578
|
+
if self.tool_count > 0:
|
|
579
|
+
result.append(" │ ", style=THEME["dim"])
|
|
580
|
+
result.append(f"🔧 {self.tool_count}", style=STREAM_COLORS["tool"])
|
|
581
|
+
|
|
582
|
+
if self.message_length > 0:
|
|
583
|
+
result.append(" │ ", style=THEME["dim"])
|
|
584
|
+
result.append(f"💬 {self.message_length} chars", style=THEME["muted"])
|
|
585
|
+
|
|
586
|
+
return result
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
# ============================================================================
|
|
590
|
+
# COLORFUL DIFF PREVIEW
|
|
591
|
+
# ============================================================================
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
class StreamDiffPreview(Static):
|
|
595
|
+
"""Colorful diff preview for file changes."""
|
|
596
|
+
|
|
597
|
+
DEFAULT_CSS = """
|
|
598
|
+
StreamDiffPreview {
|
|
599
|
+
height: auto;
|
|
600
|
+
padding: 1;
|
|
601
|
+
background: #0d0d0d;
|
|
602
|
+
border: round #2a2a2a;
|
|
603
|
+
margin: 1 0;
|
|
604
|
+
}
|
|
605
|
+
"""
|
|
606
|
+
|
|
607
|
+
def __init__(self, path: str, old_text: str, new_text: str, **kwargs):
|
|
608
|
+
super().__init__(**kwargs)
|
|
609
|
+
self.path = path
|
|
610
|
+
self.old_text = old_text
|
|
611
|
+
self.new_text = new_text
|
|
612
|
+
|
|
613
|
+
def render(self) -> Text:
|
|
614
|
+
result = Text()
|
|
615
|
+
|
|
616
|
+
# Header
|
|
617
|
+
result.append("📄 ", style=f"bold {THEME['cyan']}")
|
|
618
|
+
result.append(self.path, style=f"bold {THEME['cyan']}")
|
|
619
|
+
result.append("\n", style="")
|
|
620
|
+
|
|
621
|
+
# Calculate stats
|
|
622
|
+
old_lines = self.old_text.split("\n") if self.old_text else []
|
|
623
|
+
new_lines = self.new_text.split("\n") if self.new_text else []
|
|
624
|
+
|
|
625
|
+
result.append(f" ", style="")
|
|
626
|
+
result.append(f"+{len(new_lines)}", style=f"bold {THEME['green']}")
|
|
627
|
+
result.append(" / ", style=THEME["muted"])
|
|
628
|
+
result.append(f"-{len(old_lines)}", style=f"bold {THEME['red']}")
|
|
629
|
+
result.append(" lines\n\n", style=THEME["muted"])
|
|
630
|
+
|
|
631
|
+
# Show diff preview (first few lines)
|
|
632
|
+
for line in old_lines[:3]:
|
|
633
|
+
line_preview = line[:60] + "..." if len(line) > 60 else line
|
|
634
|
+
result.append(f" - {line_preview}\n", style=f"on #2d1f1f {THEME['red']}")
|
|
635
|
+
|
|
636
|
+
if len(old_lines) > 3:
|
|
637
|
+
result.append(f" ... ({len(old_lines) - 3} more lines)\n", style=THEME["dim"])
|
|
638
|
+
|
|
639
|
+
result.append("\n", style="")
|
|
640
|
+
|
|
641
|
+
for line in new_lines[:3]:
|
|
642
|
+
line_preview = line[:60] + "..." if len(line) > 60 else line
|
|
643
|
+
result.append(f" + {line_preview}\n", style=f"on #1f2d1f {THEME['green']}")
|
|
644
|
+
|
|
645
|
+
if len(new_lines) > 3:
|
|
646
|
+
result.append(f" ... ({len(new_lines) - 3} more lines)\n", style=THEME["dim"])
|
|
647
|
+
|
|
648
|
+
return result
|