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/app/widgets.py
ADDED
|
@@ -0,0 +1,1591 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SuperQode App Widgets - All UI widget classes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
import random
|
|
9
|
+
from time import monotonic
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from textual.widgets import Static, RichLog
|
|
13
|
+
from textual.reactive import reactive
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.markdown import Markdown
|
|
17
|
+
from rich.console import Group
|
|
18
|
+
from rich.box import ROUNDED, HEAVY
|
|
19
|
+
|
|
20
|
+
from .constants import (
|
|
21
|
+
ASCII_LOGO,
|
|
22
|
+
TAGLINE_PART1,
|
|
23
|
+
GRADIENT,
|
|
24
|
+
RAINBOW,
|
|
25
|
+
THEME,
|
|
26
|
+
ICONS,
|
|
27
|
+
AGENT_COLORS,
|
|
28
|
+
AGENT_ICONS,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GradientLogo(Static):
|
|
33
|
+
"""ASCII logo with purpleâpinkâorange gradient - BIG display."""
|
|
34
|
+
|
|
35
|
+
def render(self) -> Text:
|
|
36
|
+
# Split and filter empty lines, but preserve leading whitespace
|
|
37
|
+
lines = [line for line in ASCII_LOGO.split("\n") if line.strip()]
|
|
38
|
+
result = Text()
|
|
39
|
+
|
|
40
|
+
for i, line in enumerate(lines):
|
|
41
|
+
color = GRADIENT[i % len(GRADIENT)]
|
|
42
|
+
result.append(line, style=f"bold {color}")
|
|
43
|
+
if i < len(lines) - 1:
|
|
44
|
+
result.append("\n")
|
|
45
|
+
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ColorfulStatusBar(Static):
|
|
50
|
+
"""Colorful SuperQode status bar - always visible at top with BYOK status."""
|
|
51
|
+
|
|
52
|
+
# BYOK status properties
|
|
53
|
+
byok_provider: reactive[str] = reactive("")
|
|
54
|
+
byok_model: reactive[str] = reactive("")
|
|
55
|
+
byok_tokens: reactive[int] = reactive(0)
|
|
56
|
+
byok_cost: reactive[float] = reactive(0.0)
|
|
57
|
+
|
|
58
|
+
def render(self) -> Text:
|
|
59
|
+
result = Text()
|
|
60
|
+
|
|
61
|
+
# Logo part - with gradient colors
|
|
62
|
+
super_colors = ["#a855f7", "#b366f9", "#c177fb", "#cf88fd", "#dd99ff"]
|
|
63
|
+
for i, char in enumerate("Super"):
|
|
64
|
+
color = super_colors[i % len(super_colors)]
|
|
65
|
+
result.append(char, style=f"bold {color}")
|
|
66
|
+
qode_colors = ["#ec4899", "#f472b6", "#f97316", "#fb923c"]
|
|
67
|
+
for i, char in enumerate("Qode"):
|
|
68
|
+
color = qode_colors[i % len(qode_colors)]
|
|
69
|
+
result.append(char, style=f"bold {color}")
|
|
70
|
+
result.append(" â¨", style="bold #fbbf24")
|
|
71
|
+
result.append(" ", style="")
|
|
72
|
+
# "Multi-Agentic" in normal text color (no gradient)
|
|
73
|
+
result.append("Multi-Agentic", style="")
|
|
74
|
+
result.append(" Orchestration of ", style=THEME["muted"])
|
|
75
|
+
# "Coding Agents" in normal text color (no gradient)
|
|
76
|
+
result.append("Coding Agents", style="")
|
|
77
|
+
|
|
78
|
+
# BYOK status (if connected)
|
|
79
|
+
if self.byok_provider:
|
|
80
|
+
result.append(" â ", style="#3f3f46")
|
|
81
|
+
result.append(f"{self.byok_provider}", style="bold #10b981")
|
|
82
|
+
if self.byok_model:
|
|
83
|
+
# Show shortened model name
|
|
84
|
+
model_short = (
|
|
85
|
+
self.byok_model.split("-")[0]
|
|
86
|
+
if "-" in self.byok_model
|
|
87
|
+
else self.byok_model[:12]
|
|
88
|
+
)
|
|
89
|
+
result.append(f"/{model_short}", style="#a1a1aa")
|
|
90
|
+
|
|
91
|
+
# Show usage
|
|
92
|
+
if self.byok_tokens > 0:
|
|
93
|
+
result.append(" ", style="")
|
|
94
|
+
if self.byok_tokens >= 1000:
|
|
95
|
+
result.append(f"{self.byok_tokens // 1000}K", style="#06b6d4")
|
|
96
|
+
else:
|
|
97
|
+
result.append(f"{self.byok_tokens}", style="#06b6d4")
|
|
98
|
+
result.append(" tok", style="#52525b")
|
|
99
|
+
|
|
100
|
+
# Show cost
|
|
101
|
+
if self.byok_cost > 0:
|
|
102
|
+
result.append(" ", style="")
|
|
103
|
+
if self.byok_cost >= 0.01:
|
|
104
|
+
result.append(f"${self.byok_cost:.2f}", style="#fbbf24")
|
|
105
|
+
else:
|
|
106
|
+
result.append(f"${self.byok_cost:.3f}", style="#fbbf24")
|
|
107
|
+
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
def update_byok_status(
|
|
111
|
+
self, provider: str = "", model: str = "", tokens: int = 0, cost: float = 0.0
|
|
112
|
+
):
|
|
113
|
+
"""Update BYOK status display."""
|
|
114
|
+
self.byok_provider = provider
|
|
115
|
+
self.byok_model = model
|
|
116
|
+
self.byok_tokens = tokens
|
|
117
|
+
self.byok_cost = cost
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class GradientTagline(Static):
|
|
121
|
+
"""Tagline with gradient colors for visual impact."""
|
|
122
|
+
|
|
123
|
+
PART1_GRADIENT = ["#06b6d4", "#0ea5e9", "#3b82f6", "#6366f1", "#8b5cf6", "#a855f7"]
|
|
124
|
+
PART2_GRADIENT = ["#fbbf24", "#f59e0b", "#f97316", "#ef4444", "#ec4899"]
|
|
125
|
+
|
|
126
|
+
def render(self) -> Text:
|
|
127
|
+
result = Text()
|
|
128
|
+
result.append("đ ", style="bold #06b6d4")
|
|
129
|
+
|
|
130
|
+
part1 = TAGLINE_PART1
|
|
131
|
+
for i, char in enumerate(part1):
|
|
132
|
+
color_idx = int(i / len(part1) * len(self.PART1_GRADIENT))
|
|
133
|
+
color_idx = min(color_idx, len(self.PART1_GRADIENT) - 1)
|
|
134
|
+
color = self.PART1_GRADIENT[color_idx]
|
|
135
|
+
result.append(char, style=f"bold {color}")
|
|
136
|
+
|
|
137
|
+
result.append(" âĸ ", style="bold #71717a")
|
|
138
|
+
|
|
139
|
+
part2 = "Automate Your SDLC"
|
|
140
|
+
for i, char in enumerate(part2):
|
|
141
|
+
color_idx = int(i / len(part2) * len(self.PART2_GRADIENT))
|
|
142
|
+
color_idx = min(color_idx, len(self.PART2_GRADIENT) - 1)
|
|
143
|
+
color = self.PART2_GRADIENT[color_idx]
|
|
144
|
+
result.append(char, style=f"bold {color}")
|
|
145
|
+
|
|
146
|
+
result.append(" â¨", style="bold #fbbf24")
|
|
147
|
+
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class PulseWaveBar(Static):
|
|
152
|
+
"""Animated pulse wave bar - unique SuperQode style."""
|
|
153
|
+
|
|
154
|
+
frame = reactive(0)
|
|
155
|
+
WAVE_CHARS = "âââââ
ââââââ
ââââ"
|
|
156
|
+
PULSE_COLORS = [
|
|
157
|
+
"#7c3aed",
|
|
158
|
+
"#8b5cf6",
|
|
159
|
+
"#a855f7",
|
|
160
|
+
"#c026d3",
|
|
161
|
+
"#d946ef",
|
|
162
|
+
"#ec4899",
|
|
163
|
+
"#f472b6",
|
|
164
|
+
"#fbbf24",
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
def on_mount(self):
|
|
168
|
+
self.auto_refresh = 1 / 20
|
|
169
|
+
|
|
170
|
+
def render(self) -> Text:
|
|
171
|
+
width = self.size.width or 80
|
|
172
|
+
t = monotonic()
|
|
173
|
+
result = Text()
|
|
174
|
+
wave_len = len(self.WAVE_CHARS)
|
|
175
|
+
|
|
176
|
+
for i in range(width):
|
|
177
|
+
wave_primary = math.sin(t * 2 + i * 0.15) * 0.5
|
|
178
|
+
wave_secondary = math.sin(t * 4 + i * 0.25 + math.pi / 3) * 0.3
|
|
179
|
+
wave_tertiary = math.sin(t * 1.5 + i * 0.08 + math.pi / 2) * 0.2
|
|
180
|
+
combined = (wave_primary + wave_secondary + wave_tertiary + 1.2) / 2.4
|
|
181
|
+
combined = max(0, min(1, combined))
|
|
182
|
+
char_idx = int(combined * (wave_len - 1))
|
|
183
|
+
char = self.WAVE_CHARS[char_idx]
|
|
184
|
+
color_pos = (i / width + t * 0.15) % 1.0
|
|
185
|
+
color_idx = int(color_pos * len(self.PULSE_COLORS)) % len(self.PULSE_COLORS)
|
|
186
|
+
color = self.PULSE_COLORS[color_idx]
|
|
187
|
+
|
|
188
|
+
if char in "â
âââ":
|
|
189
|
+
result.append(char, style=f"bold {color}")
|
|
190
|
+
elif char in "ââ":
|
|
191
|
+
result.append(char, style=color)
|
|
192
|
+
else:
|
|
193
|
+
result.append(char, style=f"dim {color}")
|
|
194
|
+
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# Alias for backward compatibility
|
|
199
|
+
RainbowProgressBar = PulseWaveBar
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class ScanningLine(Static):
|
|
203
|
+
"""Scanning line that sweeps left-to-right like a radar."""
|
|
204
|
+
|
|
205
|
+
is_active = reactive(False)
|
|
206
|
+
needs_approval = reactive(False)
|
|
207
|
+
|
|
208
|
+
SCAN_COLORS = ["#a855f7", "#c026d3", "#ec4899", "#f472b6"]
|
|
209
|
+
APPROVAL_COLORS = ["#f59e0b", "#fbbf24", "#f97316", "#ef4444"]
|
|
210
|
+
|
|
211
|
+
def on_mount(self):
|
|
212
|
+
self.auto_refresh = 1 / 30
|
|
213
|
+
|
|
214
|
+
def render(self) -> Text:
|
|
215
|
+
if not self.is_active:
|
|
216
|
+
return Text("")
|
|
217
|
+
|
|
218
|
+
width = self.size.width or 80
|
|
219
|
+
t = monotonic()
|
|
220
|
+
result = Text()
|
|
221
|
+
|
|
222
|
+
speed = 0.6 if self.needs_approval else 0.4
|
|
223
|
+
scan_pos = (t * speed) % 1.0
|
|
224
|
+
scan_x = int(scan_pos * width)
|
|
225
|
+
trail_len = 12
|
|
226
|
+
|
|
227
|
+
for i in range(width):
|
|
228
|
+
dist = scan_x - i
|
|
229
|
+
if dist < 0:
|
|
230
|
+
dist += width
|
|
231
|
+
|
|
232
|
+
if dist == 0:
|
|
233
|
+
result.append("â", style="bold #ffffff")
|
|
234
|
+
elif dist > 0 and dist <= trail_len:
|
|
235
|
+
fade = 1.0 - (dist / trail_len)
|
|
236
|
+
if self.needs_approval:
|
|
237
|
+
if fade > 0.7:
|
|
238
|
+
result.append("â", style="bold #fbbf24")
|
|
239
|
+
elif fade > 0.4:
|
|
240
|
+
result.append("â", style="#f59e0b")
|
|
241
|
+
elif fade > 0.2:
|
|
242
|
+
result.append("â", style="#f97316")
|
|
243
|
+
else:
|
|
244
|
+
result.append("â", style="#7c2d12")
|
|
245
|
+
else:
|
|
246
|
+
if fade > 0.7:
|
|
247
|
+
result.append("â", style="bold #ec4899")
|
|
248
|
+
elif fade > 0.4:
|
|
249
|
+
result.append("â", style="#c026d3")
|
|
250
|
+
elif fade > 0.2:
|
|
251
|
+
result.append("â", style="#a855f7")
|
|
252
|
+
else:
|
|
253
|
+
result.append("â", style="#4a1a6b")
|
|
254
|
+
else:
|
|
255
|
+
if self.needs_approval:
|
|
256
|
+
bg_color = "#2a1a00" if int(t * 4) % 2 == 0 else "#1a1a1a"
|
|
257
|
+
result.append("â", style=bg_color)
|
|
258
|
+
else:
|
|
259
|
+
result.append("â", style="#1a1a1a")
|
|
260
|
+
|
|
261
|
+
return result
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class TopScanningLine(Static):
|
|
265
|
+
"""Top scanning line - flowing wave animation."""
|
|
266
|
+
|
|
267
|
+
is_active = reactive(False)
|
|
268
|
+
needs_approval = reactive(False)
|
|
269
|
+
WAVE_COLORS = ["#3b82f6", "#6366f1", "#8b5cf6", "#a855f7", "#c026d3", "#ec4899"]
|
|
270
|
+
|
|
271
|
+
def on_mount(self):
|
|
272
|
+
self.auto_refresh = 1 / 25
|
|
273
|
+
|
|
274
|
+
def render(self) -> Text:
|
|
275
|
+
if not self.is_active:
|
|
276
|
+
return Text("")
|
|
277
|
+
|
|
278
|
+
width = self.size.width or 80
|
|
279
|
+
t = monotonic()
|
|
280
|
+
result = Text()
|
|
281
|
+
|
|
282
|
+
for i in range(width):
|
|
283
|
+
wave1 = math.sin(t * 2 + i * 0.2) * 0.4
|
|
284
|
+
wave2 = math.sin(t * 3.5 + i * 0.15 + 1.5) * 0.3
|
|
285
|
+
combined = (wave1 + wave2 + 1) / 2
|
|
286
|
+
combined = max(0, min(1, combined))
|
|
287
|
+
|
|
288
|
+
if combined > 0.8:
|
|
289
|
+
char = "â"
|
|
290
|
+
elif combined > 0.6:
|
|
291
|
+
char = "â"
|
|
292
|
+
elif combined > 0.4:
|
|
293
|
+
char = "â"
|
|
294
|
+
elif combined > 0.2:
|
|
295
|
+
char = "â"
|
|
296
|
+
else:
|
|
297
|
+
char = "â"
|
|
298
|
+
|
|
299
|
+
color_pos = (i / width + t * 0.1) % 1.0
|
|
300
|
+
color_idx = int(color_pos * len(self.WAVE_COLORS)) % len(self.WAVE_COLORS)
|
|
301
|
+
color = self.WAVE_COLORS[color_idx]
|
|
302
|
+
|
|
303
|
+
if char in "ââ":
|
|
304
|
+
result.append(char, style=f"bold {color}")
|
|
305
|
+
elif char == "â":
|
|
306
|
+
result.append(char, style=color)
|
|
307
|
+
else:
|
|
308
|
+
result.append(char, style=f"dim {color}")
|
|
309
|
+
|
|
310
|
+
return result
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class BottomScanningLine(Static):
|
|
314
|
+
"""Bottom scanning line - radar sweep animation."""
|
|
315
|
+
|
|
316
|
+
is_active = reactive(False)
|
|
317
|
+
needs_approval = reactive(False)
|
|
318
|
+
|
|
319
|
+
def on_mount(self):
|
|
320
|
+
self.auto_refresh = 1 / 30
|
|
321
|
+
|
|
322
|
+
def render(self) -> Text:
|
|
323
|
+
if not self.is_active:
|
|
324
|
+
return Text("")
|
|
325
|
+
|
|
326
|
+
width = self.size.width or 80
|
|
327
|
+
t = monotonic()
|
|
328
|
+
result = Text()
|
|
329
|
+
|
|
330
|
+
sweep_pos = (t * 0.5) % 1.0
|
|
331
|
+
sweep_x = int(sweep_pos * width)
|
|
332
|
+
|
|
333
|
+
for i in range(width):
|
|
334
|
+
dist = abs(i - sweep_x)
|
|
335
|
+
|
|
336
|
+
if dist == 0:
|
|
337
|
+
result.append("â", style="bold #ffffff")
|
|
338
|
+
elif dist <= 3:
|
|
339
|
+
fade = 1.0 - (dist / 3.0)
|
|
340
|
+
if fade > 0.7:
|
|
341
|
+
result.append("â", style="bold #ec4899")
|
|
342
|
+
elif fade > 0.4:
|
|
343
|
+
result.append("â", style="#c026d3")
|
|
344
|
+
elif fade > 0.2:
|
|
345
|
+
result.append("â", style="#a855f7")
|
|
346
|
+
else:
|
|
347
|
+
result.append("â", style="#4a1a6b")
|
|
348
|
+
elif dist <= 8:
|
|
349
|
+
fade = 1.0 - ((dist - 3) / 5.0)
|
|
350
|
+
if fade > 0.7:
|
|
351
|
+
result.append("â", style="bold #ec4899")
|
|
352
|
+
elif fade > 0.4:
|
|
353
|
+
result.append("â", style="#c026d3")
|
|
354
|
+
elif fade > 0.2:
|
|
355
|
+
result.append("â", style="#a855f7")
|
|
356
|
+
else:
|
|
357
|
+
result.append("â", style="#4a1a6b")
|
|
358
|
+
else:
|
|
359
|
+
result.append("â", style="#1a1a1a")
|
|
360
|
+
|
|
361
|
+
return result
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# Aliases for compatibility
|
|
365
|
+
ProgressChase = ScanningLine
|
|
366
|
+
SparkleTrail = ScanningLine
|
|
367
|
+
ThinkingWave = ScanningLine
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class StreamingThinkingIndicator(Static):
|
|
371
|
+
"""Animated thinking indicator for streaming."""
|
|
372
|
+
|
|
373
|
+
is_active = reactive(False)
|
|
374
|
+
SPINNER_FRAMES = ["â ", "â ", "â š", "â ¸", "â ŧ", "â ´", "â Ļ", "â §", "â ", "â "]
|
|
375
|
+
|
|
376
|
+
THINKING_PHRASES = [
|
|
377
|
+
"đ§ Thinking deeply",
|
|
378
|
+
"đ Processing your request",
|
|
379
|
+
"⥠Analyzing the problem",
|
|
380
|
+
"đ Understanding context",
|
|
381
|
+
"⨠Generating response",
|
|
382
|
+
"đ¯ Computing solution",
|
|
383
|
+
"đ Working on it",
|
|
384
|
+
"đĄ Light bulb moment",
|
|
385
|
+
"đĒ Juggling possibilities",
|
|
386
|
+
"đ¨ Painting a masterpiece",
|
|
387
|
+
"đ§Š Solving the puzzle",
|
|
388
|
+
"đ¨âđŗ Cooking up magic",
|
|
389
|
+
"đ Launching into orbit",
|
|
390
|
+
"đĒ Casting a spell",
|
|
391
|
+
"đģ Compiling thoughts",
|
|
392
|
+
"đ§ Tightening the bolts",
|
|
393
|
+
"đ Busy bee mode",
|
|
394
|
+
"đī¸ Under construction",
|
|
395
|
+
"đ§ââī¸ Wizarding up a solution",
|
|
396
|
+
"đĻ Summoning unicorn power",
|
|
397
|
+
"đ Awakening the code dragon",
|
|
398
|
+
"đ Aligning the stars",
|
|
399
|
+
"đ Scanning the codeverse",
|
|
400
|
+
"âī¸ Splitting atoms of logic",
|
|
401
|
+
"đ Exploring the galaxy",
|
|
402
|
+
"đ¸ Beaming down answers",
|
|
403
|
+
"đŽ Consulting the crystal ball",
|
|
404
|
+
"đŦ Directing the scene",
|
|
405
|
+
"đ¸ Jamming on your code",
|
|
406
|
+
"đ˛ Rolling for initiative",
|
|
407
|
+
"đŗ Frying some fresh code",
|
|
408
|
+
"â Brewing the perfect response",
|
|
409
|
+
"đ Serving hot code",
|
|
410
|
+
"đĻ Being clever like a fox",
|
|
411
|
+
"đ Multitasking like an octopus",
|
|
412
|
+
"đĻ
Eagle-eye analyzing",
|
|
413
|
+
"đĨ Firing up the engines",
|
|
414
|
+
"đ Polishing the gem",
|
|
415
|
+
"đ Getting into character",
|
|
416
|
+
"đĄ Spinning up ideas",
|
|
417
|
+
"đ¯ Locking onto target",
|
|
418
|
+
"âī¸ Processing information",
|
|
419
|
+
"đ§Ē Experimenting with solutions",
|
|
420
|
+
"đŦ Running analysis",
|
|
421
|
+
"đ Crunching numbers",
|
|
422
|
+
"đ¨ Creating art",
|
|
423
|
+
"đĒ Performing magic",
|
|
424
|
+
"đ Acting out the solution",
|
|
425
|
+
]
|
|
426
|
+
|
|
427
|
+
def on_mount(self):
|
|
428
|
+
self.auto_refresh = 1 / 15
|
|
429
|
+
|
|
430
|
+
def render(self) -> Text:
|
|
431
|
+
if not self.is_active:
|
|
432
|
+
return Text("")
|
|
433
|
+
|
|
434
|
+
t = monotonic()
|
|
435
|
+
result = Text()
|
|
436
|
+
|
|
437
|
+
spinner_idx = int(t * 10) % len(self.SPINNER_FRAMES)
|
|
438
|
+
phrase_idx = int(t / 1.5) % len(self.THINKING_PHRASES)
|
|
439
|
+
|
|
440
|
+
colors = [
|
|
441
|
+
"#a855f7",
|
|
442
|
+
"#c026d3",
|
|
443
|
+
"#d946ef",
|
|
444
|
+
"#ec4899",
|
|
445
|
+
"#f97316",
|
|
446
|
+
"#fbbf24",
|
|
447
|
+
"#22c55e",
|
|
448
|
+
"#06b6d4",
|
|
449
|
+
]
|
|
450
|
+
color = colors[int(t * 4) % len(colors)]
|
|
451
|
+
|
|
452
|
+
spinner = self.SPINNER_FRAMES[spinner_idx]
|
|
453
|
+
phrase = self.THINKING_PHRASES[phrase_idx]
|
|
454
|
+
|
|
455
|
+
dot_count = int(t * 3) % 4
|
|
456
|
+
dots = "." * dot_count
|
|
457
|
+
|
|
458
|
+
sparkles = ["â¨", "â", "đĢ", "đ"]
|
|
459
|
+
sparkle = sparkles[int(t * 2) % len(sparkles)]
|
|
460
|
+
|
|
461
|
+
result.append(f" {spinner} ", style=f"bold {color}")
|
|
462
|
+
result.append(phrase, style=f"bold {color}")
|
|
463
|
+
result.append(dots, style=color)
|
|
464
|
+
result.append(f" {sparkle}", style=color)
|
|
465
|
+
result.append(" ", style="")
|
|
466
|
+
|
|
467
|
+
return result
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
class ModeBadge(Static):
|
|
471
|
+
"""Shows current mode with rich styling and connection info."""
|
|
472
|
+
|
|
473
|
+
mode = reactive("home")
|
|
474
|
+
role = reactive("")
|
|
475
|
+
agent = reactive("")
|
|
476
|
+
model = reactive("")
|
|
477
|
+
provider = reactive("")
|
|
478
|
+
execution_mode = reactive("")
|
|
479
|
+
approval_mode = reactive("auto")
|
|
480
|
+
|
|
481
|
+
def render(self) -> Text:
|
|
482
|
+
t = Text()
|
|
483
|
+
|
|
484
|
+
if self.execution_mode == "pure":
|
|
485
|
+
t.append(" đ§Ē ", style=f"bold {THEME['pink']}")
|
|
486
|
+
t.append("PURE", style=f"bold {THEME['pink']} reverse")
|
|
487
|
+
t.append(" âĸ ", style=THEME["muted"])
|
|
488
|
+
|
|
489
|
+
if self.provider:
|
|
490
|
+
t.append(f"{self.provider.upper()}", style=f"bold {THEME['cyan']}")
|
|
491
|
+
|
|
492
|
+
if self.model:
|
|
493
|
+
t.append(" ", style="")
|
|
494
|
+
t.append(f"đ {self.model}", style=THEME["muted"])
|
|
495
|
+
|
|
496
|
+
return t
|
|
497
|
+
|
|
498
|
+
if self.agent:
|
|
499
|
+
color = AGENT_COLORS.get(self.agent, THEME["purple"])
|
|
500
|
+
icon = AGENT_ICONS.get(self.agent, "đ¤")
|
|
501
|
+
|
|
502
|
+
if self.execution_mode == "acp":
|
|
503
|
+
t.append(" đ ", style=f"bold {THEME['cyan']}")
|
|
504
|
+
t.append("ACP", style=f"bold {THEME['cyan']} reverse")
|
|
505
|
+
t.append(" âĸ ", style=THEME["muted"])
|
|
506
|
+
elif self.execution_mode == "byok":
|
|
507
|
+
t.append(" ⥠", style=f"bold {THEME['success']}")
|
|
508
|
+
t.append("BYOK", style=f"bold {THEME['success']} reverse")
|
|
509
|
+
t.append(" âĸ ", style=THEME["muted"])
|
|
510
|
+
|
|
511
|
+
t.append(f"{icon} ", style=f"bold {color}")
|
|
512
|
+
t.append(self.agent.upper(), style=f"bold {color}")
|
|
513
|
+
|
|
514
|
+
if self.model:
|
|
515
|
+
t.append(" ", style="")
|
|
516
|
+
t.append(f"đ {self.model}", style=THEME["muted"])
|
|
517
|
+
if self.provider:
|
|
518
|
+
t.append(" ", style="")
|
|
519
|
+
t.append(f"âī¸ {self.provider}", style=THEME["dim"])
|
|
520
|
+
|
|
521
|
+
mode_icons = {"auto": "đĸ", "ask": "đĄ", "deny": "đ´"}
|
|
522
|
+
mode_colors = {
|
|
523
|
+
"auto": THEME["success"],
|
|
524
|
+
"ask": THEME["warning"],
|
|
525
|
+
"deny": THEME["error"],
|
|
526
|
+
}
|
|
527
|
+
approval_icon = mode_icons.get(self.approval_mode, "đĄ")
|
|
528
|
+
approval_color = mode_colors.get(self.approval_mode, THEME["warning"])
|
|
529
|
+
t.append(" ", style="")
|
|
530
|
+
t.append(f"{approval_icon}", style=approval_color)
|
|
531
|
+
|
|
532
|
+
elif self.role:
|
|
533
|
+
mode_styles = {
|
|
534
|
+
"dev": (ICONS["dev"], THEME["success"], "đģ"),
|
|
535
|
+
"qa": (ICONS["qa"], THEME["orange"], "đ§Ē"),
|
|
536
|
+
"devops": (ICONS["devops"], THEME["cyan"], "âī¸"),
|
|
537
|
+
}
|
|
538
|
+
icon, color, emoji = mode_styles.get(self.mode, (ICONS["home"], THEME["purple"], "đ "))
|
|
539
|
+
|
|
540
|
+
if self.execution_mode == "acp":
|
|
541
|
+
t.append(" đ ", style=f"bold {THEME['cyan']}")
|
|
542
|
+
t.append("ACP", style=f"bold {THEME['cyan']} reverse")
|
|
543
|
+
t.append(" âĸ ", style=THEME["muted"])
|
|
544
|
+
elif self.execution_mode == "byok":
|
|
545
|
+
t.append(" ⥠", style=f"bold {THEME['success']}")
|
|
546
|
+
t.append("BYOK", style=f"bold {THEME['success']} reverse")
|
|
547
|
+
t.append(" âĸ ", style=THEME["muted"])
|
|
548
|
+
|
|
549
|
+
t.append(f"{emoji} ", style=f"bold {color}")
|
|
550
|
+
t.append(f"{self.mode.upper()}", style=f"bold {color}")
|
|
551
|
+
t.append(" âē ", style=THEME["muted"])
|
|
552
|
+
t.append(self.role, style=f"bold {color}")
|
|
553
|
+
|
|
554
|
+
if self.model:
|
|
555
|
+
t.append(" ", style="")
|
|
556
|
+
t.append(f"đ {self.model}", style=THEME["dim"])
|
|
557
|
+
|
|
558
|
+
mode_icons = {"auto": "đĸ", "ask": "đĄ", "deny": "đ´"}
|
|
559
|
+
mode_colors = {
|
|
560
|
+
"auto": THEME["success"],
|
|
561
|
+
"ask": THEME["warning"],
|
|
562
|
+
"deny": THEME["error"],
|
|
563
|
+
}
|
|
564
|
+
approval_icon = mode_icons.get(self.approval_mode, "đĄ")
|
|
565
|
+
approval_color = mode_colors.get(self.approval_mode, THEME["warning"])
|
|
566
|
+
t.append(" ", style="")
|
|
567
|
+
t.append(f"{approval_icon}", style=approval_color)
|
|
568
|
+
else:
|
|
569
|
+
t.append(f" đ ", style=f"bold {THEME['purple']}")
|
|
570
|
+
t.append("HOME", style=f"bold {THEME['purple']} reverse")
|
|
571
|
+
t.append(" ", style="")
|
|
572
|
+
t.append("ready to code", style=f"dim {THEME['muted']}")
|
|
573
|
+
|
|
574
|
+
return t
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class HintsBar(Static):
|
|
578
|
+
"""Context hints with gradient colors and emojis."""
|
|
579
|
+
|
|
580
|
+
approval_mode = reactive("auto")
|
|
581
|
+
|
|
582
|
+
def render(self) -> Text:
|
|
583
|
+
t = Text()
|
|
584
|
+
|
|
585
|
+
# t.append("\n", style="")
|
|
586
|
+
|
|
587
|
+
hints = [
|
|
588
|
+
("đ :home", THEME["cyan"]),
|
|
589
|
+
("â :h [:help]", THEME["purple"]),
|
|
590
|
+
("đ :i [:init]", THEME["success"]),
|
|
591
|
+
("đ :s [:sidebar]", THEME["cyan"]),
|
|
592
|
+
("đ :c [:connect]", THEME["pink"]),
|
|
593
|
+
("đ :q [:quit]", THEME["orange"]),
|
|
594
|
+
]
|
|
595
|
+
for i, (hint, color) in enumerate(hints):
|
|
596
|
+
if i > 0:
|
|
597
|
+
t.append(" âĸ ", style=THEME["dim"])
|
|
598
|
+
t.append(hint, style=color)
|
|
599
|
+
|
|
600
|
+
return t
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
class SelectableTextArea(Static):
|
|
604
|
+
"""A text area that allows mouse selection and copying.
|
|
605
|
+
|
|
606
|
+
Used as a popup overlay when user wants to select/copy text.
|
|
607
|
+
"""
|
|
608
|
+
|
|
609
|
+
DEFAULT_CSS = """
|
|
610
|
+
SelectableTextArea {
|
|
611
|
+
background: #0a0a0a;
|
|
612
|
+
border: round #7c3aed;
|
|
613
|
+
padding: 1 2;
|
|
614
|
+
width: 80%;
|
|
615
|
+
height: 80%;
|
|
616
|
+
layer: overlay;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
SelectableTextArea .title {
|
|
620
|
+
text-align: center;
|
|
621
|
+
color: #a855f7;
|
|
622
|
+
text-style: bold;
|
|
623
|
+
margin-bottom: 1;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
SelectableTextArea .hint {
|
|
627
|
+
text-align: center;
|
|
628
|
+
color: #71717a;
|
|
629
|
+
margin-top: 1;
|
|
630
|
+
}
|
|
631
|
+
"""
|
|
632
|
+
|
|
633
|
+
def __init__(self, content: str, title: str = "Response", **kwargs):
|
|
634
|
+
super().__init__(**kwargs)
|
|
635
|
+
self._content = content
|
|
636
|
+
self._title = title
|
|
637
|
+
|
|
638
|
+
def render(self) -> Text:
|
|
639
|
+
t = Text()
|
|
640
|
+
t.append(f"đ {self._title}\n", style=f"bold {THEME['purple']}")
|
|
641
|
+
t.append("â" * 40 + "\n\n", style=THEME["border"])
|
|
642
|
+
t.append(self._content, style=THEME["text"])
|
|
643
|
+
t.append("\n\n" + "â" * 40 + "\n", style=THEME["border"])
|
|
644
|
+
t.append(
|
|
645
|
+
"Hold Shift + drag to select âĸ Ctrl+C to copy âĸ Escape to close", style=THEME["muted"]
|
|
646
|
+
)
|
|
647
|
+
return t
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
class ConversationLog(RichLog):
|
|
651
|
+
"""Chat log with styled messages and rich formatting.
|
|
652
|
+
|
|
653
|
+
Text Selection:
|
|
654
|
+
- Hold Shift while dragging to select text (terminal native selection)
|
|
655
|
+
- Ctrl+Shift+C to copy last response
|
|
656
|
+
- :copy command to copy to clipboard
|
|
657
|
+
- :select to open selectable view
|
|
658
|
+
"""
|
|
659
|
+
|
|
660
|
+
DEFAULT_CSS = """
|
|
661
|
+
ConversationLog {
|
|
662
|
+
scrollbar-gutter: stable;
|
|
663
|
+
background: #000000;
|
|
664
|
+
width: 100%;
|
|
665
|
+
padding: 0;
|
|
666
|
+
margin: 0;
|
|
667
|
+
}
|
|
668
|
+
"""
|
|
669
|
+
|
|
670
|
+
def __init__(self, *args, **kwargs):
|
|
671
|
+
# Remove any width-related kwargs that might limit display
|
|
672
|
+
kwargs.pop("max_width", None)
|
|
673
|
+
kwargs.pop("width", None)
|
|
674
|
+
super().__init__(*args, **kwargs)
|
|
675
|
+
# Track messages for copy functionality
|
|
676
|
+
self._messages: list[tuple[str, str, str]] = [] # (role, text, agent_name)
|
|
677
|
+
self._last_response: str = ""
|
|
678
|
+
self._last_error: str = "" # Track last error for easy copy
|
|
679
|
+
# Track thinking and tool calls for agent sessions
|
|
680
|
+
self._thinking_lines: list[str] = []
|
|
681
|
+
self._tool_calls: list[dict] = []
|
|
682
|
+
self._streaming_response: str = ""
|
|
683
|
+
# Force console width to None (unlimited) immediately after init
|
|
684
|
+
self._force_unlimited_width = True
|
|
685
|
+
|
|
686
|
+
def on_mount(self) -> None:
|
|
687
|
+
"""Configure console width when widget is mounted."""
|
|
688
|
+
super().on_mount()
|
|
689
|
+
# Override Rich's internal console width to use full available width
|
|
690
|
+
# RichLog uses an internal Console that might have a default width limit
|
|
691
|
+
# Try multiple possible attribute names for the internal console
|
|
692
|
+
self._update_console_width()
|
|
693
|
+
# Also try to set it after a small delay to ensure it's applied
|
|
694
|
+
self.set_timer(0.1, self._update_console_width)
|
|
695
|
+
|
|
696
|
+
def _update_console_width(self) -> None:
|
|
697
|
+
"""Update the internal Rich console width - FORCE UNLIMITED (NO CHARACTER LIMITS)."""
|
|
698
|
+
# Get actual terminal width - use a very large value to prevent truncation
|
|
699
|
+
import shutil
|
|
700
|
+
|
|
701
|
+
try:
|
|
702
|
+
terminal_width = shutil.get_terminal_size().columns
|
|
703
|
+
# Use terminal width * 2 to ensure no truncation, minimum 200
|
|
704
|
+
target_width = max(terminal_width * 2, 200) if terminal_width > 0 else 500
|
|
705
|
+
except Exception:
|
|
706
|
+
target_width = 500 # Large fallback
|
|
707
|
+
|
|
708
|
+
# Set console width on all possible console attributes
|
|
709
|
+
console_attrs = [
|
|
710
|
+
"_console",
|
|
711
|
+
"console",
|
|
712
|
+
"_rich_console",
|
|
713
|
+
"rich_console",
|
|
714
|
+
"_log_console",
|
|
715
|
+
"log_console",
|
|
716
|
+
]
|
|
717
|
+
|
|
718
|
+
for attr in console_attrs:
|
|
719
|
+
try:
|
|
720
|
+
if hasattr(self, attr):
|
|
721
|
+
console = getattr(self, attr)
|
|
722
|
+
if console and hasattr(console, "width"):
|
|
723
|
+
console.width = target_width
|
|
724
|
+
if hasattr(console, "legacy_width"):
|
|
725
|
+
console.legacy_width = target_width
|
|
726
|
+
if hasattr(console, "max_width"):
|
|
727
|
+
console.max_width = target_width
|
|
728
|
+
# Also set soft_wrap to True for natural wrapping
|
|
729
|
+
if hasattr(console, "soft_wrap"):
|
|
730
|
+
console.soft_wrap = True
|
|
731
|
+
except Exception:
|
|
732
|
+
continue
|
|
733
|
+
|
|
734
|
+
# Also try to access console through various internal attributes
|
|
735
|
+
internal_attrs = ["_renderable", "_log", "_buffer", "_output"]
|
|
736
|
+
for attr in internal_attrs:
|
|
737
|
+
try:
|
|
738
|
+
if hasattr(self, attr):
|
|
739
|
+
obj = getattr(self, attr)
|
|
740
|
+
if hasattr(obj, "_console"):
|
|
741
|
+
obj._console.width = target_width
|
|
742
|
+
if hasattr(obj, "console"):
|
|
743
|
+
obj.console.width = target_width
|
|
744
|
+
except Exception:
|
|
745
|
+
continue
|
|
746
|
+
|
|
747
|
+
# Try to access through __dict__ to find any console-like objects
|
|
748
|
+
try:
|
|
749
|
+
for key, value in self.__dict__.items():
|
|
750
|
+
if "console" in key.lower() and hasattr(value, "width"):
|
|
751
|
+
value.width = target_width
|
|
752
|
+
except Exception:
|
|
753
|
+
pass
|
|
754
|
+
|
|
755
|
+
def on_resize(self, event) -> None:
|
|
756
|
+
"""Update console width when widget is resized."""
|
|
757
|
+
super().on_resize(event)
|
|
758
|
+
# Update console width when widget size changes
|
|
759
|
+
self._update_console_width()
|
|
760
|
+
|
|
761
|
+
def add_user(self, text: str):
|
|
762
|
+
self._messages.append(("user", text, ""))
|
|
763
|
+
# Use None for width to allow full width usage
|
|
764
|
+
panel = Panel(
|
|
765
|
+
Text(text, style=THEME["text"], overflow="fold"),
|
|
766
|
+
title=f"[bold {THEME['cyan']}]đŠâđģđ¨âđģ >[/]",
|
|
767
|
+
border_style=THEME["border"],
|
|
768
|
+
box=ROUNDED,
|
|
769
|
+
padding=(0, 1),
|
|
770
|
+
width=None, # Use full available width
|
|
771
|
+
)
|
|
772
|
+
self.write(panel)
|
|
773
|
+
|
|
774
|
+
def add_agent(self, text: str, agent: str = "Agent"):
|
|
775
|
+
self._messages.append(("agent", text, agent))
|
|
776
|
+
self._last_response = text # Track for easy copy
|
|
777
|
+
color = AGENT_COLORS.get(agent.lower(), THEME["purple"])
|
|
778
|
+
icon = AGENT_ICONS.get(agent.lower(), "đ¤")
|
|
779
|
+
# Use overflow="fold" to wrap instead of truncate
|
|
780
|
+
content = (
|
|
781
|
+
Markdown(text) if "```" in text else Text(text, style=THEME["text"], overflow="fold")
|
|
782
|
+
)
|
|
783
|
+
panel = Panel(
|
|
784
|
+
content,
|
|
785
|
+
title=f"[bold {color}]{icon} {agent} Agent[/]",
|
|
786
|
+
border_style=color,
|
|
787
|
+
box=ROUNDED,
|
|
788
|
+
padding=(0, 1),
|
|
789
|
+
width=None, # Use full available width
|
|
790
|
+
)
|
|
791
|
+
self.write(panel)
|
|
792
|
+
|
|
793
|
+
def add_assistant(self, text: str, agent: str = "Assistant"):
|
|
794
|
+
"""Alias for add_agent - used by TUI for assistant responses."""
|
|
795
|
+
self.add_agent(text, agent)
|
|
796
|
+
|
|
797
|
+
def write(self, *args, **kwargs):
|
|
798
|
+
"""Override write to ensure console width is always correct - NO LIMITS."""
|
|
799
|
+
# Ensure console width is updated before writing
|
|
800
|
+
self._update_console_width()
|
|
801
|
+
|
|
802
|
+
# Process args to ensure Text objects have proper overflow handling
|
|
803
|
+
processed_args = []
|
|
804
|
+
for arg in args:
|
|
805
|
+
if isinstance(arg, Text):
|
|
806
|
+
# Ensure Text uses fold overflow for natural wrapping
|
|
807
|
+
if not hasattr(arg, "overflow") or arg.overflow != "fold":
|
|
808
|
+
arg.overflow = "fold"
|
|
809
|
+
processed_args.append(arg)
|
|
810
|
+
|
|
811
|
+
return super().write(*processed_args, **kwargs)
|
|
812
|
+
|
|
813
|
+
def add_system(self, text: str):
|
|
814
|
+
self._messages.append(("system", text, ""))
|
|
815
|
+
self.write(Text(f" ⨠{text}", style=f"italic {THEME['muted']}"))
|
|
816
|
+
|
|
817
|
+
def add_error(self, text: str):
|
|
818
|
+
self._messages.append(("error", text, ""))
|
|
819
|
+
self._last_error = text # Track for easy copy
|
|
820
|
+
|
|
821
|
+
# Try to display with rich markup support
|
|
822
|
+
try:
|
|
823
|
+
self.write(Text(f" â {text}", markup=True))
|
|
824
|
+
except Exception:
|
|
825
|
+
# Fallback to plain text
|
|
826
|
+
self.write(Text(f" â {text}", style=THEME["error"]))
|
|
827
|
+
|
|
828
|
+
def add_success(self, text: str):
|
|
829
|
+
self._messages.append(("success", text, ""))
|
|
830
|
+
self.write(Text(f" â
{text}", style=THEME["success"]))
|
|
831
|
+
|
|
832
|
+
def add_info(self, text: str):
|
|
833
|
+
self._messages.append(("info", text, ""))
|
|
834
|
+
self.write(Text(f" âšī¸ {text}", style=THEME["cyan"]))
|
|
835
|
+
|
|
836
|
+
def add_shell(self, cmd: str, output: str, ok: bool = True):
|
|
837
|
+
"""Add shell command output to the log."""
|
|
838
|
+
self._messages.append(("shell", f"{cmd}\n{output}", ""))
|
|
839
|
+
status_icon = "âĄ" if ok else "đĨ"
|
|
840
|
+
status_style = THEME["success"] if ok else THEME["error"]
|
|
841
|
+
|
|
842
|
+
# Format command and output with distinct styling
|
|
843
|
+
content = Text.assemble(
|
|
844
|
+
(f" {status_icon} ", status_style),
|
|
845
|
+
(f"Terminal", f"bold {THEME['cyan']}"),
|
|
846
|
+
(": ", THEME["muted"]),
|
|
847
|
+
(f"{cmd}\n", f"bold {THEME['text']}"),
|
|
848
|
+
(output, THEME["text"] if ok else THEME["error"]),
|
|
849
|
+
)
|
|
850
|
+
self.write(content)
|
|
851
|
+
|
|
852
|
+
def get_last_response(self) -> str:
|
|
853
|
+
"""Get the last agent response text for copying."""
|
|
854
|
+
return self._last_response
|
|
855
|
+
|
|
856
|
+
def get_last_error(self) -> str:
|
|
857
|
+
"""Get the last error text for copying."""
|
|
858
|
+
return self._last_error
|
|
859
|
+
|
|
860
|
+
def get_last_message(self, role: str = None) -> str:
|
|
861
|
+
"""Get the last message, optionally filtered by role."""
|
|
862
|
+
if not self._messages:
|
|
863
|
+
return ""
|
|
864
|
+
if role:
|
|
865
|
+
for msg_role, text, _ in reversed(self._messages):
|
|
866
|
+
if msg_role == role:
|
|
867
|
+
return text
|
|
868
|
+
return ""
|
|
869
|
+
return self._messages[-1][1]
|
|
870
|
+
|
|
871
|
+
def get_all_text(self) -> str:
|
|
872
|
+
"""Get all messages as plain text for export."""
|
|
873
|
+
lines = []
|
|
874
|
+
for role, text, agent in self._messages:
|
|
875
|
+
if role == "user":
|
|
876
|
+
lines.append(f"You: {text}")
|
|
877
|
+
elif role == "agent":
|
|
878
|
+
lines.append(f"{agent or 'Agent'}: {text}")
|
|
879
|
+
elif role == "system":
|
|
880
|
+
lines.append(f"System: {text}")
|
|
881
|
+
elif role == "error":
|
|
882
|
+
lines.append(f"Error: {text}")
|
|
883
|
+
elif role == "success":
|
|
884
|
+
lines.append(f"Success: {text}")
|
|
885
|
+
elif role == "info":
|
|
886
|
+
lines.append(f"Info: {text}")
|
|
887
|
+
return "\n\n".join(lines)
|
|
888
|
+
|
|
889
|
+
def copy_to_clipboard(self, text: str = None) -> bool:
|
|
890
|
+
"""Copy text to clipboard. Returns True if successful."""
|
|
891
|
+
try:
|
|
892
|
+
import pyperclip
|
|
893
|
+
|
|
894
|
+
content = text if text is not None else self._last_response
|
|
895
|
+
if content:
|
|
896
|
+
pyperclip.copy(content)
|
|
897
|
+
return True
|
|
898
|
+
except ImportError:
|
|
899
|
+
# pyperclip not available, try platform-specific
|
|
900
|
+
try:
|
|
901
|
+
import subprocess
|
|
902
|
+
import sys
|
|
903
|
+
|
|
904
|
+
content = text if text is not None else self._last_response
|
|
905
|
+
if not content:
|
|
906
|
+
return False
|
|
907
|
+
if sys.platform == "darwin":
|
|
908
|
+
subprocess.run(["pbcopy"], input=content.encode(), check=True)
|
|
909
|
+
return True
|
|
910
|
+
elif sys.platform.startswith("linux"):
|
|
911
|
+
subprocess.run(
|
|
912
|
+
["xclip", "-selection", "clipboard"], input=content.encode(), check=True
|
|
913
|
+
)
|
|
914
|
+
return True
|
|
915
|
+
elif sys.platform == "win32":
|
|
916
|
+
subprocess.run(["clip"], input=content.encode(), check=True)
|
|
917
|
+
return True
|
|
918
|
+
except Exception:
|
|
919
|
+
pass
|
|
920
|
+
except Exception:
|
|
921
|
+
pass
|
|
922
|
+
return False
|
|
923
|
+
|
|
924
|
+
def add_tool_approval_needed(self, tool_name: str, description: str = ""):
|
|
925
|
+
"""Display a prominent inline notification that tool approval is needed."""
|
|
926
|
+
self.write(
|
|
927
|
+
Panel(
|
|
928
|
+
Text.assemble(
|
|
929
|
+
("â ī¸ ACTION REQUIRED\n\n", f"bold {THEME['warning']}"),
|
|
930
|
+
("Tool: ", THEME["muted"]),
|
|
931
|
+
(f"{tool_name}\n", f"bold {THEME['cyan']}"),
|
|
932
|
+
(f"{description}\n\n" if description else "\n", THEME["text"]),
|
|
933
|
+
("â ", f"bold {THEME['warning']}"),
|
|
934
|
+
("Type in prompt box above: ", THEME["text"]),
|
|
935
|
+
("y", f"bold {THEME['success']}"),
|
|
936
|
+
(" to approve, ", THEME["muted"]),
|
|
937
|
+
("n", f"bold {THEME['error']}"),
|
|
938
|
+
(" to reject", THEME["muted"]),
|
|
939
|
+
),
|
|
940
|
+
title=f"[bold {THEME['warning']}]đ Tool Approval Needed[/]",
|
|
941
|
+
border_style=THEME["warning"],
|
|
942
|
+
box=HEAVY,
|
|
943
|
+
padding=(1, 2),
|
|
944
|
+
)
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
# =========================================================================
|
|
948
|
+
# ENHANCED STREAMING OUTPUT METHODS
|
|
949
|
+
# These methods provide better display for agent thinking and responses
|
|
950
|
+
# Compatible with BYOK, ACP, and Local modes
|
|
951
|
+
# =========================================================================
|
|
952
|
+
|
|
953
|
+
def start_agent_session(
|
|
954
|
+
self,
|
|
955
|
+
agent_name: str,
|
|
956
|
+
model_name: str = "",
|
|
957
|
+
mode: str = "acp",
|
|
958
|
+
approval_mode: str = "ask",
|
|
959
|
+
):
|
|
960
|
+
"""
|
|
961
|
+
Start a new agent output session with header.
|
|
962
|
+
|
|
963
|
+
Args:
|
|
964
|
+
agent_name: Name of the agent (e.g., "OpenCode", "Claude")
|
|
965
|
+
model_name: Model being used (e.g., "gpt-4o", "claude-sonnet")
|
|
966
|
+
mode: Connection mode ("acp", "byok", "local")
|
|
967
|
+
approval_mode: Approval mode ("auto", "ask", "deny")
|
|
968
|
+
"""
|
|
969
|
+
# Reset streaming state
|
|
970
|
+
self._streaming_response = ""
|
|
971
|
+
self._streaming_thinking = ""
|
|
972
|
+
self._thinking_lines = []
|
|
973
|
+
self._tool_calls = []
|
|
974
|
+
self._session_start_time = None
|
|
975
|
+
|
|
976
|
+
try:
|
|
977
|
+
from time import monotonic
|
|
978
|
+
|
|
979
|
+
self._session_start_time = monotonic()
|
|
980
|
+
except Exception:
|
|
981
|
+
pass
|
|
982
|
+
|
|
983
|
+
# Mode badges
|
|
984
|
+
mode_badges = {
|
|
985
|
+
"acp": ("đ", "ACP", THEME["success"]),
|
|
986
|
+
"byok": ("đ", "BYOK", THEME["cyan"]),
|
|
987
|
+
"local": ("đģ", "Local", THEME["warning"]),
|
|
988
|
+
}
|
|
989
|
+
mode_icon, mode_label, mode_color = mode_badges.get(
|
|
990
|
+
mode.lower(), ("â", mode.upper(), THEME["muted"])
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
# Approval mode
|
|
994
|
+
approval_badges = {
|
|
995
|
+
"auto": ("đĸ", "AUTO", THEME["success"]),
|
|
996
|
+
"ask": ("đĄ", "ASK", THEME["warning"]),
|
|
997
|
+
"deny": ("đ´", "DENY", THEME["error"]),
|
|
998
|
+
}
|
|
999
|
+
app_icon, app_label, app_color = approval_badges.get(
|
|
1000
|
+
approval_mode, ("đĄ", "ASK", THEME["warning"])
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
# Build header
|
|
1004
|
+
header = Text()
|
|
1005
|
+
header.append("\n")
|
|
1006
|
+
|
|
1007
|
+
# Gradient line
|
|
1008
|
+
gradient = ["#6d28d9", "#7c3aed", "#8b5cf6", "#a855f7", "#c084fc"]
|
|
1009
|
+
line = "â" * 60
|
|
1010
|
+
for i, char in enumerate(line):
|
|
1011
|
+
header.append(char, style=gradient[i % len(gradient)])
|
|
1012
|
+
header.append("\n")
|
|
1013
|
+
|
|
1014
|
+
# Agent name
|
|
1015
|
+
agent_color = AGENT_COLORS.get(agent_name.lower(), THEME["purple"])
|
|
1016
|
+
agent_icon = AGENT_ICONS.get(agent_name.lower(), "đ¤")
|
|
1017
|
+
header.append(f" {agent_icon} ", style=f"bold {agent_color}")
|
|
1018
|
+
header.append(agent_name.upper(), style=f"bold {THEME['text']}")
|
|
1019
|
+
header.append(" is working\n", style=THEME["muted"])
|
|
1020
|
+
|
|
1021
|
+
# Model and mode info
|
|
1022
|
+
header.append(" Model: ", style=THEME["dim"])
|
|
1023
|
+
header.append(model_name or "auto", style=f"bold {THEME['cyan']}")
|
|
1024
|
+
header.append(f" â {mode_icon} ", style=mode_color)
|
|
1025
|
+
header.append(mode_label, style=f"bold {mode_color}")
|
|
1026
|
+
header.append(f" â {app_icon} ", style=app_color)
|
|
1027
|
+
header.append(app_label, style=f"bold {app_color}")
|
|
1028
|
+
header.append("\n")
|
|
1029
|
+
|
|
1030
|
+
self.write(header)
|
|
1031
|
+
|
|
1032
|
+
def add_thinking(self, text: str, category: str = "general"):
|
|
1033
|
+
"""
|
|
1034
|
+
Add a thinking/reasoning line (always visible).
|
|
1035
|
+
|
|
1036
|
+
This method ALWAYS shows thinking regardless of show_thinking_logs setting
|
|
1037
|
+
because it's explicitly called for important agent reasoning.
|
|
1038
|
+
|
|
1039
|
+
Args:
|
|
1040
|
+
text: The thinking text to display
|
|
1041
|
+
category: Category for icon selection (planning, analyzing, etc.)
|
|
1042
|
+
"""
|
|
1043
|
+
if not text or not text.strip():
|
|
1044
|
+
return
|
|
1045
|
+
|
|
1046
|
+
# Ensure auto-scroll is ON
|
|
1047
|
+
self.auto_scroll = True
|
|
1048
|
+
|
|
1049
|
+
# Store for later copy
|
|
1050
|
+
self._thinking_lines.append(text)
|
|
1051
|
+
|
|
1052
|
+
# Category icons and colors
|
|
1053
|
+
category_styles = {
|
|
1054
|
+
"planning": ("đ", "#f472b6"),
|
|
1055
|
+
"analyzing": ("đŦ", "#c084fc"),
|
|
1056
|
+
"deciding": ("đ¤", "#fbbf24"),
|
|
1057
|
+
"searching": ("đ", "#60a5fa"),
|
|
1058
|
+
"reading": ("đ", "#34d399"),
|
|
1059
|
+
"writing": ("âī¸", "#818cf8"),
|
|
1060
|
+
"debugging": ("đ", "#ef4444"),
|
|
1061
|
+
"executing": ("âĄ", "#fb923c"),
|
|
1062
|
+
"verifying": ("â
", "#22c55e"),
|
|
1063
|
+
"testing": ("đ§Ē", "#a78bfa"),
|
|
1064
|
+
"refactoring": ("đ§", "#9ca3af"),
|
|
1065
|
+
"discovery": ("đ", "#06b6d4"),
|
|
1066
|
+
"thinking": ("đ§ ", "#e879f9"),
|
|
1067
|
+
"notifying": ("đ", "#facc15"),
|
|
1068
|
+
"general": ("đ", "#94a3b8"),
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
icon, color = category_styles.get(category.lower(), category_styles["general"])
|
|
1072
|
+
|
|
1073
|
+
# Auto-detect category from text if not specified (or is general)
|
|
1074
|
+
if category == "general":
|
|
1075
|
+
text_lower = text.lower()
|
|
1076
|
+
if any(w in text_lower for w in ["test", "pytest", "expect", "assertion"]):
|
|
1077
|
+
icon, color = category_styles["testing"]
|
|
1078
|
+
elif any(w in text_lower for w in ["run", "execute", "command", "bash", "shell"]):
|
|
1079
|
+
icon, color = category_styles["executing"]
|
|
1080
|
+
elif any(w in text_lower for w in ["verify", "confirm", "check", "validation"]):
|
|
1081
|
+
icon, color = category_styles["verifying"]
|
|
1082
|
+
elif any(w in text_lower for w in ["debug", "error", "fix", "bug", "traceback"]):
|
|
1083
|
+
icon, color = category_styles["debugging"]
|
|
1084
|
+
elif any(w in text_lower for w in ["plan", "step", "approach", "todo"]):
|
|
1085
|
+
icon, color = category_styles["planning"]
|
|
1086
|
+
elif any(w in text_lower for w in ["search", "find", "look", "grep", "glob"]):
|
|
1087
|
+
icon, color = category_styles["searching"]
|
|
1088
|
+
elif any(w in text_lower for w in ["read", "file", "content", "cat"]):
|
|
1089
|
+
icon, color = category_styles["reading"]
|
|
1090
|
+
elif any(w in text_lower for w in ["write", "create", "add", "edit", "save"]):
|
|
1091
|
+
icon, color = category_styles["writing"]
|
|
1092
|
+
elif any(w in text_lower for w in ["discover", "list", "explore", "scan"]):
|
|
1093
|
+
icon, color = category_styles["discovery"]
|
|
1094
|
+
elif any(w in text_lower for w in ["think", "reason", "ponder", "analyze"]):
|
|
1095
|
+
icon, color = category_styles["thinking"]
|
|
1096
|
+
elif any(w in text_lower for w in ["info", "note", "alert", "notice"]):
|
|
1097
|
+
icon, color = category_styles["notifying"]
|
|
1098
|
+
else:
|
|
1099
|
+
# Randomize generic icon to avoid repetition
|
|
1100
|
+
generic_icons = ["đ", "đĄ", "âī¸", "đ§Š", "đŽ", "â¨", "đĄ"]
|
|
1101
|
+
import random
|
|
1102
|
+
|
|
1103
|
+
icon = random.choice(generic_icons)
|
|
1104
|
+
# Keep neutral color for generic thoughts
|
|
1105
|
+
|
|
1106
|
+
# Display thinking line
|
|
1107
|
+
line = Text()
|
|
1108
|
+
line.append(f" {icon} ", style=f"bold {color}")
|
|
1109
|
+
line.append(text, style=f"italic {THEME['muted']}")
|
|
1110
|
+
line.append("\n")
|
|
1111
|
+
self.write(line)
|
|
1112
|
+
|
|
1113
|
+
def add_response_chunk(self, text: str):
|
|
1114
|
+
"""
|
|
1115
|
+
Add a chunk of response text (for streaming).
|
|
1116
|
+
|
|
1117
|
+
Accumulates chunks and displays them intelligently to avoid word-per-line display.
|
|
1118
|
+
Highlights code blocks in real-time.
|
|
1119
|
+
|
|
1120
|
+
Args:
|
|
1121
|
+
text: The response chunk to add
|
|
1122
|
+
"""
|
|
1123
|
+
if not text:
|
|
1124
|
+
return
|
|
1125
|
+
|
|
1126
|
+
self._streaming_response += text
|
|
1127
|
+
self.auto_scroll = True
|
|
1128
|
+
|
|
1129
|
+
# Check for code block state
|
|
1130
|
+
if not hasattr(self, "_in_code_block"):
|
|
1131
|
+
self._in_code_block = False
|
|
1132
|
+
|
|
1133
|
+
# Toggle code block state
|
|
1134
|
+
if "```" in text:
|
|
1135
|
+
# Count occurrences to toggle state correctly
|
|
1136
|
+
count = text.count("```")
|
|
1137
|
+
if count % 2 != 0:
|
|
1138
|
+
self._in_code_block = not self._in_code_block
|
|
1139
|
+
|
|
1140
|
+
# Buffer chunks and only write on natural boundaries to avoid word-per-line
|
|
1141
|
+
if not hasattr(self, "_chunk_buffer"):
|
|
1142
|
+
self._chunk_buffer = ""
|
|
1143
|
+
self._chunk_buffer += text
|
|
1144
|
+
|
|
1145
|
+
# Write when we have:
|
|
1146
|
+
# 1. A complete sentence (ends with . ! ? : ; followed by space or newline)
|
|
1147
|
+
# 2. A newline character in the text
|
|
1148
|
+
# 3. Accumulated enough text (50+ chars with a space near the end)
|
|
1149
|
+
buffer = self._chunk_buffer
|
|
1150
|
+
should_write = (
|
|
1151
|
+
(
|
|
1152
|
+
buffer.rstrip().endswith((".", "!", "?", ":", ";"))
|
|
1153
|
+
and (text.endswith(" ") or text.endswith("\n") or len(buffer) > 30)
|
|
1154
|
+
)
|
|
1155
|
+
or "\n" in text
|
|
1156
|
+
or (len(buffer) > 50 and " " in buffer[-15:])
|
|
1157
|
+
)
|
|
1158
|
+
|
|
1159
|
+
if should_write:
|
|
1160
|
+
chunk_text = Text()
|
|
1161
|
+
style = f"bold {THEME['cyan']}" if self._in_code_block else THEME["text"]
|
|
1162
|
+
chunk_text.append(buffer, style=style)
|
|
1163
|
+
self.write(chunk_text)
|
|
1164
|
+
self._chunk_buffer = ""
|
|
1165
|
+
|
|
1166
|
+
def flush_response_buffer(self):
|
|
1167
|
+
"""Flush any remaining buffered response chunks."""
|
|
1168
|
+
if hasattr(self, "_chunk_buffer") and self._chunk_buffer:
|
|
1169
|
+
chunk_text = Text()
|
|
1170
|
+
style = (
|
|
1171
|
+
f"bold {THEME['cyan']}" if getattr(self, "_in_code_block", False) else THEME["text"]
|
|
1172
|
+
)
|
|
1173
|
+
chunk_text.append(self._chunk_buffer, style=style)
|
|
1174
|
+
self.write(chunk_text)
|
|
1175
|
+
self._chunk_buffer = ""
|
|
1176
|
+
|
|
1177
|
+
def add_tool_call(
|
|
1178
|
+
self,
|
|
1179
|
+
tool_name: str,
|
|
1180
|
+
status: str = "running",
|
|
1181
|
+
file_path: str = "",
|
|
1182
|
+
command: str = "",
|
|
1183
|
+
output: str = "",
|
|
1184
|
+
):
|
|
1185
|
+
"""
|
|
1186
|
+
Add a tool call display.
|
|
1187
|
+
|
|
1188
|
+
Args:
|
|
1189
|
+
tool_name: Name of the tool being called
|
|
1190
|
+
status: Status ("pending", "running", "success", "error")
|
|
1191
|
+
file_path: File path if applicable
|
|
1192
|
+
command: Command if it's a shell tool
|
|
1193
|
+
output: Tool output/result
|
|
1194
|
+
"""
|
|
1195
|
+
self._tool_calls.append(
|
|
1196
|
+
{
|
|
1197
|
+
"name": tool_name,
|
|
1198
|
+
"status": status,
|
|
1199
|
+
"path": file_path,
|
|
1200
|
+
"command": command,
|
|
1201
|
+
}
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
# Track file modifications
|
|
1205
|
+
if status in ("running", "success") and file_path:
|
|
1206
|
+
# Initialize _files_modified if not exists
|
|
1207
|
+
if not hasattr(self, "_files_modified"):
|
|
1208
|
+
self._files_modified = set()
|
|
1209
|
+
|
|
1210
|
+
# Add to set if it's a write/edit operation
|
|
1211
|
+
tool_lower = tool_name.lower()
|
|
1212
|
+
if any(op in tool_lower for op in ("write", "edit", "create", "append", "patch")):
|
|
1213
|
+
self._files_modified.add(file_path)
|
|
1214
|
+
|
|
1215
|
+
# Status icons and colors
|
|
1216
|
+
status_map = {
|
|
1217
|
+
"pending": ("â", THEME["muted"]),
|
|
1218
|
+
"running": ("â", THEME["purple"]),
|
|
1219
|
+
"success": ("âĻ", THEME["success"]),
|
|
1220
|
+
"error": ("â", THEME["error"]),
|
|
1221
|
+
}
|
|
1222
|
+
status_icon, status_color = status_map.get(status, ("â", THEME["muted"]))
|
|
1223
|
+
|
|
1224
|
+
# Tool type icons
|
|
1225
|
+
tool_icons = {
|
|
1226
|
+
"read": "âŗ",
|
|
1227
|
+
"write": "â˛",
|
|
1228
|
+
"edit": "âŗ",
|
|
1229
|
+
"shell": "â¸",
|
|
1230
|
+
"bash": "â¸",
|
|
1231
|
+
"search": "â",
|
|
1232
|
+
"glob": "âŽ",
|
|
1233
|
+
"grep": "â",
|
|
1234
|
+
}
|
|
1235
|
+
tool_icon = "âĸ"
|
|
1236
|
+
for key, icon in tool_icons.items():
|
|
1237
|
+
if key in tool_name.lower():
|
|
1238
|
+
tool_icon = icon
|
|
1239
|
+
break
|
|
1240
|
+
|
|
1241
|
+
# Build display
|
|
1242
|
+
line = Text()
|
|
1243
|
+
line.append(f" {status_icon} ", style=f"bold {status_color}")
|
|
1244
|
+
line.append(f"{tool_icon} ", style=THEME["dim"])
|
|
1245
|
+
line.append(tool_name, style=THEME["text"])
|
|
1246
|
+
|
|
1247
|
+
if file_path:
|
|
1248
|
+
# Show full file path - let widget handle wrapping
|
|
1249
|
+
line.append(f" {file_path}", style=THEME["dim"])
|
|
1250
|
+
elif command:
|
|
1251
|
+
# Show more of the command - 100 chars instead of 40
|
|
1252
|
+
cmd_short = command[:100] + "..." if len(command) > 100 else command
|
|
1253
|
+
line.append(f" $ {cmd_short}", style=THEME["dim"])
|
|
1254
|
+
|
|
1255
|
+
if output and status in ("success", "error"):
|
|
1256
|
+
# Show full output - no truncation, let the widget handle wrapping
|
|
1257
|
+
line.append(f"\n â {output}", style=THEME["muted"])
|
|
1258
|
+
|
|
1259
|
+
line.append("\n")
|
|
1260
|
+
self.write(line)
|
|
1261
|
+
|
|
1262
|
+
def end_agent_session(
|
|
1263
|
+
self,
|
|
1264
|
+
success: bool = True,
|
|
1265
|
+
response_text: str = "",
|
|
1266
|
+
prompt_tokens: int = 0,
|
|
1267
|
+
completion_tokens: int = 0,
|
|
1268
|
+
thinking_tokens: int = 0,
|
|
1269
|
+
cost: float = 0.0,
|
|
1270
|
+
):
|
|
1271
|
+
"""
|
|
1272
|
+
End the agent output session with a rich Mission Report summary.
|
|
1273
|
+
|
|
1274
|
+
Args:
|
|
1275
|
+
success: Whether the session completed successfully
|
|
1276
|
+
response_text: Final response text (if not already streamed)
|
|
1277
|
+
prompt_tokens: Number of prompt tokens used
|
|
1278
|
+
completion_tokens: Number of completion tokens used
|
|
1279
|
+
thinking_tokens: Number of thinking tokens used
|
|
1280
|
+
cost: Cost in dollars
|
|
1281
|
+
"""
|
|
1282
|
+
# Flush any remaining buffered response chunks
|
|
1283
|
+
self.flush_response_buffer()
|
|
1284
|
+
|
|
1285
|
+
# Calculate duration
|
|
1286
|
+
duration = 0.0
|
|
1287
|
+
if hasattr(self, "_session_start_time") and self._session_start_time:
|
|
1288
|
+
try:
|
|
1289
|
+
from time import monotonic
|
|
1290
|
+
|
|
1291
|
+
duration = monotonic() - self._session_start_time
|
|
1292
|
+
except Exception:
|
|
1293
|
+
pass
|
|
1294
|
+
|
|
1295
|
+
# Store final response for copy
|
|
1296
|
+
if response_text:
|
|
1297
|
+
self._last_response = response_text
|
|
1298
|
+
self._streaming_response = response_text
|
|
1299
|
+
elif self._streaming_response:
|
|
1300
|
+
self._last_response = self._streaming_response
|
|
1301
|
+
|
|
1302
|
+
# Build rich summary panel
|
|
1303
|
+
summary_content = Text()
|
|
1304
|
+
|
|
1305
|
+
# 1. Header
|
|
1306
|
+
if success:
|
|
1307
|
+
summary_content.append("â
Mission Accomplished", style=f"bold {THEME['success']}")
|
|
1308
|
+
else:
|
|
1309
|
+
summary_content.append("â Mission Failed", style=f"bold {THEME['error']}")
|
|
1310
|
+
summary_content.append("\n\n")
|
|
1311
|
+
|
|
1312
|
+
# 2. Tool Usage Stats
|
|
1313
|
+
tool_counts = {}
|
|
1314
|
+
for tool in getattr(self, "_tool_calls", []):
|
|
1315
|
+
name = tool.get("name", "Unknown")
|
|
1316
|
+
tool_counts[name] = tool_counts.get(name, 0) + 1
|
|
1317
|
+
|
|
1318
|
+
if tool_counts:
|
|
1319
|
+
summary_content.append("đ ī¸ Tool Usage:\n", style="bold")
|
|
1320
|
+
for name, count in tool_counts.items():
|
|
1321
|
+
summary_content.append(f" âĸ {name}: ", style=THEME["text"])
|
|
1322
|
+
summary_content.append(f"{count}\n", style=f"bold {THEME['cyan']}")
|
|
1323
|
+
summary_content.append("\n")
|
|
1324
|
+
|
|
1325
|
+
# 3. Modified Files
|
|
1326
|
+
files_mod = getattr(self, "_files_modified", set())
|
|
1327
|
+
if files_mod:
|
|
1328
|
+
summary_content.append("đ Files Impacted:\n", style="bold")
|
|
1329
|
+
for f in sorted(files_mod):
|
|
1330
|
+
summary_content.append(f" âĸ {f}\n", style=THEME["warning"])
|
|
1331
|
+
summary_content.append("\n")
|
|
1332
|
+
|
|
1333
|
+
# 4. Performance Stats Grid
|
|
1334
|
+
summary_content.append("đ Stats:\n", style="bold")
|
|
1335
|
+
total_tokens = prompt_tokens + completion_tokens
|
|
1336
|
+
|
|
1337
|
+
stats_line = []
|
|
1338
|
+
if duration > 0:
|
|
1339
|
+
stats_line.append(f"âą {duration:.1f}s")
|
|
1340
|
+
if total_tokens > 0:
|
|
1341
|
+
stats_line.append(f"đ¤ {total_tokens:,} toks")
|
|
1342
|
+
if cost > 0:
|
|
1343
|
+
stats_line.append(f"đ° ${cost:.4f}")
|
|
1344
|
+
|
|
1345
|
+
summary_content.append(" " + " âĸ ".join(stats_line), style=THEME["dim"])
|
|
1346
|
+
summary_content.append("\n")
|
|
1347
|
+
|
|
1348
|
+
# Create panel
|
|
1349
|
+
panel = Panel(
|
|
1350
|
+
summary_content,
|
|
1351
|
+
title="[bold]Session Report[/bold]",
|
|
1352
|
+
border_style=THEME["success"] if success else THEME["error"],
|
|
1353
|
+
box=ROUNDED,
|
|
1354
|
+
padding=(1, 2),
|
|
1355
|
+
)
|
|
1356
|
+
|
|
1357
|
+
self.write(panel)
|
|
1358
|
+
|
|
1359
|
+
# Copy hint
|
|
1360
|
+
footer = Text()
|
|
1361
|
+
footer.append("\n")
|
|
1362
|
+
footer.append(
|
|
1363
|
+
" [Shift+Drag to select text] âĸ [Ctrl+Shift+C to copy full response]",
|
|
1364
|
+
style=THEME["dim"],
|
|
1365
|
+
)
|
|
1366
|
+
footer.append("\n")
|
|
1367
|
+
|
|
1368
|
+
self.write(footer)
|
|
1369
|
+
|
|
1370
|
+
def get_thinking_text(self) -> str:
|
|
1371
|
+
"""Get all thinking text for copying."""
|
|
1372
|
+
return "\n".join(getattr(self, "_thinking_lines", []))
|
|
1373
|
+
|
|
1374
|
+
def get_streaming_response(self) -> str:
|
|
1375
|
+
"""Get the accumulated streaming response."""
|
|
1376
|
+
return getattr(self, "_streaming_response", "")
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
class ApprovalWidget(Static):
|
|
1380
|
+
"""Widget for accepting/rejecting agent file changes."""
|
|
1381
|
+
|
|
1382
|
+
DEFAULT_CSS = """
|
|
1383
|
+
ApprovalWidget {
|
|
1384
|
+
height: auto;
|
|
1385
|
+
padding: 1;
|
|
1386
|
+
margin: 1 0;
|
|
1387
|
+
background: #1a1a1a;
|
|
1388
|
+
border: round #3a3a3a;
|
|
1389
|
+
}
|
|
1390
|
+
"""
|
|
1391
|
+
|
|
1392
|
+
def __init__(self, title: str, description: str = "", file_path: str = ""):
|
|
1393
|
+
super().__init__()
|
|
1394
|
+
self.title = title
|
|
1395
|
+
self.description = description
|
|
1396
|
+
self.file_path = file_path
|
|
1397
|
+
self.approved = None
|
|
1398
|
+
|
|
1399
|
+
def render(self) -> Text:
|
|
1400
|
+
t = Text()
|
|
1401
|
+
t.append(f"\n â ī¸ ", style=f"bold {THEME['warning']}")
|
|
1402
|
+
t.append("Approval Required\n", style=f"bold {THEME['warning']}")
|
|
1403
|
+
t.append(f" {self.title}\n", style=THEME["text"])
|
|
1404
|
+
if self.file_path:
|
|
1405
|
+
t.append(f" đ {self.file_path}\n", style=THEME["muted"])
|
|
1406
|
+
if self.description:
|
|
1407
|
+
t.append(f" {self.description}\n", style=THEME["dim"])
|
|
1408
|
+
t.append("\n ", style="")
|
|
1409
|
+
t.append("[A]", style=f"bold {THEME['success']}")
|
|
1410
|
+
t.append(" Accept ", style=THEME["success"])
|
|
1411
|
+
t.append("[R]", style=f"bold {THEME['error']}")
|
|
1412
|
+
t.append(" Reject ", style=THEME["error"])
|
|
1413
|
+
t.append("[E]", style=f"bold {THEME['cyan']}")
|
|
1414
|
+
t.append(" Edit ", style=THEME["cyan"])
|
|
1415
|
+
t.append("[V]", style=f"bold {THEME['purple']}")
|
|
1416
|
+
t.append(" View Diff\n", style=THEME["purple"])
|
|
1417
|
+
return t
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
class DiffDisplay(Static):
|
|
1421
|
+
"""Display code diff with syntax highlighting."""
|
|
1422
|
+
|
|
1423
|
+
def __init__(self, file_path: str, old_content: str, new_content: str):
|
|
1424
|
+
super().__init__()
|
|
1425
|
+
self.file_path = file_path
|
|
1426
|
+
self.old_content = old_content
|
|
1427
|
+
self.new_content = new_content
|
|
1428
|
+
|
|
1429
|
+
def render(self) -> Text:
|
|
1430
|
+
t = Text()
|
|
1431
|
+
|
|
1432
|
+
t.append(f"\n đ ", style=f"bold {THEME['cyan']}")
|
|
1433
|
+
t.append(f"{self.file_path}\n", style=f"bold {THEME['cyan']}")
|
|
1434
|
+
t.append(f" â" * 30 + "\n", style=THEME["border"])
|
|
1435
|
+
|
|
1436
|
+
old_lines = self.old_content.split("\n") if self.old_content else []
|
|
1437
|
+
new_lines = self.new_content.split("\n") if self.new_content else []
|
|
1438
|
+
|
|
1439
|
+
additions = 0
|
|
1440
|
+
deletions = 0
|
|
1441
|
+
|
|
1442
|
+
for line in old_lines:
|
|
1443
|
+
if line not in new_lines:
|
|
1444
|
+
t.append(f" - {line}\n", style=f"on #3d1f1f {THEME['error']}")
|
|
1445
|
+
deletions += 1
|
|
1446
|
+
|
|
1447
|
+
for line in new_lines:
|
|
1448
|
+
if line not in old_lines:
|
|
1449
|
+
t.append(f" + {line}\n", style=f"on #1f3d1f {THEME['success']}")
|
|
1450
|
+
additions += 1
|
|
1451
|
+
|
|
1452
|
+
t.append(f"\n đ ", style=THEME["cyan"])
|
|
1453
|
+
t.append(f"+{additions}", style=f"bold {THEME['success']}")
|
|
1454
|
+
t.append(" / ", style=THEME["muted"])
|
|
1455
|
+
t.append(f"-{deletions}", style=f"bold {THEME['error']}")
|
|
1456
|
+
t.append(" lines changed\n", style=THEME["muted"])
|
|
1457
|
+
|
|
1458
|
+
return t
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
class PlanDisplay(Static):
|
|
1462
|
+
"""Display agent's plan with task status."""
|
|
1463
|
+
|
|
1464
|
+
def __init__(self, tasks: list[dict[str, Any]]):
|
|
1465
|
+
super().__init__()
|
|
1466
|
+
self.tasks = tasks
|
|
1467
|
+
|
|
1468
|
+
def render(self) -> Text:
|
|
1469
|
+
t = Text()
|
|
1470
|
+
t.append(f"\n đ ", style=f"bold {THEME['purple']}")
|
|
1471
|
+
t.append("Agent Plan\n", style=f"bold {THEME['purple']}")
|
|
1472
|
+
t.append(f" â" * 25 + "\n", style=THEME["border"])
|
|
1473
|
+
|
|
1474
|
+
status_icons = {
|
|
1475
|
+
"pending": ("âŗ", THEME["muted"]),
|
|
1476
|
+
"in_progress": ("đ", THEME["cyan"]),
|
|
1477
|
+
"completed": ("â
", THEME["success"]),
|
|
1478
|
+
"failed": ("â", THEME["error"]),
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
for i, task in enumerate(self.tasks, 1):
|
|
1482
|
+
status = task.get("status", "pending")
|
|
1483
|
+
icon, color = status_icons.get(status, ("â", THEME["muted"]))
|
|
1484
|
+
priority = task.get("priority", "medium")
|
|
1485
|
+
|
|
1486
|
+
priority_badges = {
|
|
1487
|
+
"high": ("đ´", THEME["error"]),
|
|
1488
|
+
"medium": ("đĄ", THEME["warning"]),
|
|
1489
|
+
"low": ("đĸ", THEME["success"]),
|
|
1490
|
+
}
|
|
1491
|
+
p_icon, p_color = priority_badges.get(priority, ("â", THEME["muted"]))
|
|
1492
|
+
|
|
1493
|
+
t.append(f" {icon} ", style=color)
|
|
1494
|
+
t.append(f"{i}. ", style=THEME["muted"])
|
|
1495
|
+
t.append(
|
|
1496
|
+
task.get("content", "Task"), style=color if status == "completed" else THEME["text"]
|
|
1497
|
+
)
|
|
1498
|
+
t.append(f" {p_icon}\n", style=p_color)
|
|
1499
|
+
|
|
1500
|
+
return t
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
class ToolCallDisplay(Static):
|
|
1504
|
+
"""Display tool calls made by the agent."""
|
|
1505
|
+
|
|
1506
|
+
def __init__(self, tool_name: str, status: str = "pending", title: str = "", content: str = ""):
|
|
1507
|
+
super().__init__()
|
|
1508
|
+
self.tool_name = tool_name
|
|
1509
|
+
self.status = status
|
|
1510
|
+
self.title = title or tool_name
|
|
1511
|
+
self.content = content
|
|
1512
|
+
|
|
1513
|
+
def render(self) -> Text:
|
|
1514
|
+
t = Text()
|
|
1515
|
+
|
|
1516
|
+
status_styles = {
|
|
1517
|
+
"pending": ("âŗ", THEME["muted"]),
|
|
1518
|
+
"in_progress": ("đ", THEME["cyan"]),
|
|
1519
|
+
"completed": ("â
", THEME["success"]),
|
|
1520
|
+
"failed": ("â", THEME["error"]),
|
|
1521
|
+
}
|
|
1522
|
+
icon, color = status_styles.get(self.status, ("đ§", THEME["purple"]))
|
|
1523
|
+
|
|
1524
|
+
t.append(f" {icon} ", style=color)
|
|
1525
|
+
t.append("đ§ ", style=THEME["orange"])
|
|
1526
|
+
t.append(self.title, style=f"bold {color}")
|
|
1527
|
+
|
|
1528
|
+
if self.status == "completed":
|
|
1529
|
+
t.append(" â", style=THEME["success"])
|
|
1530
|
+
elif self.status == "failed":
|
|
1531
|
+
t.append(" â", style=THEME["error"])
|
|
1532
|
+
|
|
1533
|
+
t.append("\n", style="")
|
|
1534
|
+
|
|
1535
|
+
if self.content:
|
|
1536
|
+
content = self.content[:200] + "..." if len(self.content) > 200 else self.content
|
|
1537
|
+
t.append(f" {content}\n", style=THEME["dim"])
|
|
1538
|
+
|
|
1539
|
+
return t
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
class FlashMessage(Static):
|
|
1543
|
+
"""Quick flash notification message."""
|
|
1544
|
+
|
|
1545
|
+
def __init__(self, message: str, style: str = "default"):
|
|
1546
|
+
super().__init__()
|
|
1547
|
+
self.message = message
|
|
1548
|
+
self.flash_style = style
|
|
1549
|
+
|
|
1550
|
+
def render(self) -> Text:
|
|
1551
|
+
t = Text()
|
|
1552
|
+
|
|
1553
|
+
style_config = {
|
|
1554
|
+
"default": ("âšī¸", THEME["cyan"], "#0a2a3a"),
|
|
1555
|
+
"success": ("â
", THEME["success"], "#0a3a1a"),
|
|
1556
|
+
"warning": ("â ī¸", THEME["warning"], "#3a2a0a"),
|
|
1557
|
+
"error": ("â", THEME["error"], "#3a0a0a"),
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
icon, color, _ = style_config.get(self.flash_style, style_config["default"])
|
|
1561
|
+
|
|
1562
|
+
t.append(f" {icon} ", style=f"bold {color}")
|
|
1563
|
+
t.append(self.message, style=f"{color}")
|
|
1564
|
+
|
|
1565
|
+
return t
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
class DangerWarning(Static):
|
|
1569
|
+
"""Warning for dangerous operations."""
|
|
1570
|
+
|
|
1571
|
+
def __init__(self, level: str = "warning", message: str = ""):
|
|
1572
|
+
super().__init__()
|
|
1573
|
+
self.level = level
|
|
1574
|
+
self.message = message
|
|
1575
|
+
|
|
1576
|
+
def render(self) -> Text:
|
|
1577
|
+
t = Text()
|
|
1578
|
+
|
|
1579
|
+
if self.level == "destructive":
|
|
1580
|
+
t.append(f"\n đ¨ ", style=f"bold {THEME['error']}")
|
|
1581
|
+
t.append("DESTRUCTIVE OPERATION", style=f"bold {THEME['error']}")
|
|
1582
|
+
t.append("\n May alter files outside project directory!\n", style=THEME["error"])
|
|
1583
|
+
else:
|
|
1584
|
+
t.append(f"\n â ī¸ ", style=f"bold {THEME['warning']}")
|
|
1585
|
+
t.append("Potentially Dangerous", style=f"bold {THEME['warning']}")
|
|
1586
|
+
t.append("\n Please review carefully before approving.\n", style=THEME["warning"])
|
|
1587
|
+
|
|
1588
|
+
if self.message:
|
|
1589
|
+
t.append(f" {self.message}\n", style=THEME["muted"])
|
|
1590
|
+
|
|
1591
|
+
return t
|