superqode 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- superqode-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QE Dashboard Widget - Quality Metrics Visualization.
|
|
3
|
+
|
|
4
|
+
A SuperQode-original widget showing real-time quality metrics
|
|
5
|
+
during QE sessions. Displays coverage, complexity, tech debt,
|
|
6
|
+
and active analysis progress.
|
|
7
|
+
|
|
8
|
+
Design: Distinctive SuperQode visualization that doesn't copy
|
|
9
|
+
from other coding agent tools.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import List, Optional
|
|
16
|
+
|
|
17
|
+
from rich.console import RenderableType
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
from rich.text import Text
|
|
21
|
+
from rich.progress import Progress, BarColumn, TextColumn, SpinnerColumn
|
|
22
|
+
from textual.reactive import reactive
|
|
23
|
+
from textual.widgets import Static
|
|
24
|
+
from textual.timer import Timer
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MetricStatus(Enum):
|
|
28
|
+
"""Status indicator for quality metrics."""
|
|
29
|
+
|
|
30
|
+
EXCELLENT = "excellent" # Green
|
|
31
|
+
GOOD = "good" # Blue
|
|
32
|
+
WARNING = "warning" # Yellow
|
|
33
|
+
CRITICAL = "critical" # Red
|
|
34
|
+
UNKNOWN = "unknown" # Gray
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class QualityMetric:
|
|
39
|
+
"""A single quality metric."""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
value: float # 0.0 to 100.0
|
|
43
|
+
label: str = "" # e.g., "82%", "Low", "A"
|
|
44
|
+
status: MetricStatus = MetricStatus.UNKNOWN
|
|
45
|
+
trend: str = "" # "↑", "↓", "→"
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def coverage(cls, value: float) -> "QualityMetric":
|
|
49
|
+
"""Create a coverage metric."""
|
|
50
|
+
if value >= 80:
|
|
51
|
+
status = MetricStatus.EXCELLENT
|
|
52
|
+
elif value >= 60:
|
|
53
|
+
status = MetricStatus.GOOD
|
|
54
|
+
elif value >= 40:
|
|
55
|
+
status = MetricStatus.WARNING
|
|
56
|
+
else:
|
|
57
|
+
status = MetricStatus.CRITICAL
|
|
58
|
+
return cls("Coverage", value, f"{value:.0f}%", status)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def complexity(cls, value: float) -> "QualityMetric":
|
|
62
|
+
"""Create a complexity metric (lower is better)."""
|
|
63
|
+
if value <= 10:
|
|
64
|
+
status, label = MetricStatus.EXCELLENT, "Low"
|
|
65
|
+
elif value <= 20:
|
|
66
|
+
status, label = MetricStatus.GOOD, "Medium"
|
|
67
|
+
elif value <= 30:
|
|
68
|
+
status, label = MetricStatus.WARNING, "High"
|
|
69
|
+
else:
|
|
70
|
+
status, label = MetricStatus.CRITICAL, "Very High"
|
|
71
|
+
return cls("Complexity", value, label, status)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def tech_debt(cls, value: float) -> "QualityMetric":
|
|
75
|
+
"""Create a tech debt metric (lower is better)."""
|
|
76
|
+
if value <= 15:
|
|
77
|
+
status = MetricStatus.EXCELLENT
|
|
78
|
+
elif value <= 30:
|
|
79
|
+
status = MetricStatus.GOOD
|
|
80
|
+
elif value <= 50:
|
|
81
|
+
status = MetricStatus.WARNING
|
|
82
|
+
else:
|
|
83
|
+
status = MetricStatus.CRITICAL
|
|
84
|
+
return cls("Tech Debt", value, f"{value:.0f}%", status)
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def test_health(cls, value: float) -> "QualityMetric":
|
|
88
|
+
"""Create a test health metric."""
|
|
89
|
+
if value >= 90:
|
|
90
|
+
status = MetricStatus.EXCELLENT
|
|
91
|
+
elif value >= 75:
|
|
92
|
+
status = MetricStatus.GOOD
|
|
93
|
+
elif value >= 50:
|
|
94
|
+
status = MetricStatus.WARNING
|
|
95
|
+
else:
|
|
96
|
+
status = MetricStatus.CRITICAL
|
|
97
|
+
return cls("Test Health", value, f"{value:.0f}%", status)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class AnalysisTask:
|
|
102
|
+
"""An active analysis task."""
|
|
103
|
+
|
|
104
|
+
file_path: str
|
|
105
|
+
description: str
|
|
106
|
+
progress: float = 0.0 # 0.0 to 1.0
|
|
107
|
+
started_at: datetime = field(default_factory=datetime.now)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Color palette for SuperQode branding
|
|
111
|
+
METRIC_COLORS = {
|
|
112
|
+
MetricStatus.EXCELLENT: "#22c55e", # Green
|
|
113
|
+
MetricStatus.GOOD: "#3b82f6", # Blue
|
|
114
|
+
MetricStatus.WARNING: "#eab308", # Yellow
|
|
115
|
+
MetricStatus.CRITICAL: "#ef4444", # Red
|
|
116
|
+
MetricStatus.UNKNOWN: "#6b7280", # Gray
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class QEDashboard(Static):
|
|
121
|
+
"""Quality Engineering Dashboard Widget.
|
|
122
|
+
|
|
123
|
+
Displays real-time quality metrics and active analysis progress
|
|
124
|
+
in a compact, informative panel.
|
|
125
|
+
|
|
126
|
+
Usage:
|
|
127
|
+
dashboard = QEDashboard()
|
|
128
|
+
dashboard.update_metric(QualityMetric.coverage(82))
|
|
129
|
+
dashboard.set_active_analysis("src/api/handlers.py", "Checking boundaries...")
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
DEFAULT_CSS = """
|
|
133
|
+
QEDashboard {
|
|
134
|
+
height: auto;
|
|
135
|
+
border: solid #3f3f46;
|
|
136
|
+
padding: 0 1;
|
|
137
|
+
margin: 0 0 1 0;
|
|
138
|
+
}
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
# Reactive state
|
|
142
|
+
metrics: reactive[List[QualityMetric]] = reactive(list)
|
|
143
|
+
active_task: reactive[Optional[AnalysisTask]] = reactive(None)
|
|
144
|
+
is_analyzing: reactive[bool] = reactive(False)
|
|
145
|
+
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
title: str = "Quality Pulse",
|
|
149
|
+
compact: bool = False,
|
|
150
|
+
**kwargs,
|
|
151
|
+
):
|
|
152
|
+
super().__init__(**kwargs)
|
|
153
|
+
self.title = title
|
|
154
|
+
self.compact = compact
|
|
155
|
+
self._animation_frame = 0
|
|
156
|
+
self._timer: Optional[Timer] = None
|
|
157
|
+
|
|
158
|
+
# Default metrics
|
|
159
|
+
self._metrics: List[QualityMetric] = [
|
|
160
|
+
QualityMetric.coverage(0),
|
|
161
|
+
QualityMetric.complexity(0),
|
|
162
|
+
QualityMetric.tech_debt(0),
|
|
163
|
+
QualityMetric.test_health(0),
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
def on_mount(self) -> None:
|
|
167
|
+
"""Start animation timer when mounted."""
|
|
168
|
+
self._timer = self.set_interval(0.1, self._tick, pause=True)
|
|
169
|
+
|
|
170
|
+
def _tick(self) -> None:
|
|
171
|
+
"""Animation tick."""
|
|
172
|
+
self._animation_frame += 1
|
|
173
|
+
if self.active_task:
|
|
174
|
+
self.refresh()
|
|
175
|
+
|
|
176
|
+
def update_metric(self, metric: QualityMetric) -> None:
|
|
177
|
+
"""Update a specific metric by name."""
|
|
178
|
+
for i, m in enumerate(self._metrics):
|
|
179
|
+
if m.name == metric.name:
|
|
180
|
+
self._metrics[i] = metric
|
|
181
|
+
break
|
|
182
|
+
else:
|
|
183
|
+
self._metrics.append(metric)
|
|
184
|
+
self.refresh()
|
|
185
|
+
|
|
186
|
+
def set_metrics(
|
|
187
|
+
self,
|
|
188
|
+
coverage: Optional[float] = None,
|
|
189
|
+
complexity: Optional[float] = None,
|
|
190
|
+
tech_debt: Optional[float] = None,
|
|
191
|
+
test_health: Optional[float] = None,
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Convenience method to set multiple metrics at once."""
|
|
194
|
+
if coverage is not None:
|
|
195
|
+
self.update_metric(QualityMetric.coverage(coverage))
|
|
196
|
+
if complexity is not None:
|
|
197
|
+
self.update_metric(QualityMetric.complexity(complexity))
|
|
198
|
+
if tech_debt is not None:
|
|
199
|
+
self.update_metric(QualityMetric.tech_debt(tech_debt))
|
|
200
|
+
if test_health is not None:
|
|
201
|
+
self.update_metric(QualityMetric.test_health(test_health))
|
|
202
|
+
|
|
203
|
+
def set_active_analysis(
|
|
204
|
+
self,
|
|
205
|
+
file_path: str,
|
|
206
|
+
description: str,
|
|
207
|
+
progress: float = 0.0,
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Set the current active analysis task."""
|
|
210
|
+
self.active_task = AnalysisTask(
|
|
211
|
+
file_path=file_path,
|
|
212
|
+
description=description,
|
|
213
|
+
progress=progress,
|
|
214
|
+
)
|
|
215
|
+
self.is_analyzing = True
|
|
216
|
+
if self._timer:
|
|
217
|
+
self._timer.resume()
|
|
218
|
+
self.refresh()
|
|
219
|
+
|
|
220
|
+
def update_progress(self, progress: float, description: str = "") -> None:
|
|
221
|
+
"""Update the progress of the active analysis."""
|
|
222
|
+
if self.active_task:
|
|
223
|
+
self.active_task.progress = progress
|
|
224
|
+
if description:
|
|
225
|
+
self.active_task.description = description
|
|
226
|
+
self.refresh()
|
|
227
|
+
|
|
228
|
+
def clear_analysis(self) -> None:
|
|
229
|
+
"""Clear the active analysis task."""
|
|
230
|
+
self.active_task = None
|
|
231
|
+
self.is_analyzing = False
|
|
232
|
+
if self._timer:
|
|
233
|
+
self._timer.pause()
|
|
234
|
+
self.refresh()
|
|
235
|
+
|
|
236
|
+
def _render_progress_bar(self, value: float, width: int = 10) -> Text:
|
|
237
|
+
"""Render a progress bar with Unicode blocks."""
|
|
238
|
+
filled = int(value / 100 * width)
|
|
239
|
+
empty = width - filled
|
|
240
|
+
|
|
241
|
+
bar = Text()
|
|
242
|
+
bar.append("█" * filled, style="bold #22c55e")
|
|
243
|
+
bar.append("░" * empty, style="#3f3f46")
|
|
244
|
+
return bar
|
|
245
|
+
|
|
246
|
+
def _render_metric(self, metric: QualityMetric) -> Text:
|
|
247
|
+
"""Render a single metric."""
|
|
248
|
+
color = METRIC_COLORS[metric.status]
|
|
249
|
+
|
|
250
|
+
result = Text()
|
|
251
|
+
result.append(f"{metric.name}: ", style="#a1a1aa")
|
|
252
|
+
result.append(self._render_progress_bar(metric.value))
|
|
253
|
+
result.append(f" {metric.label}", style=f"bold {color}")
|
|
254
|
+
|
|
255
|
+
if metric.trend:
|
|
256
|
+
result.append(f" {metric.trend}", style="#6b7280")
|
|
257
|
+
|
|
258
|
+
return result
|
|
259
|
+
|
|
260
|
+
def _get_analysis_spinner(self) -> str:
|
|
261
|
+
"""Get animated spinner character."""
|
|
262
|
+
spinners = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
263
|
+
return spinners[self._animation_frame % len(spinners)]
|
|
264
|
+
|
|
265
|
+
def render(self) -> RenderableType:
|
|
266
|
+
"""Render the dashboard."""
|
|
267
|
+
content = Text()
|
|
268
|
+
|
|
269
|
+
# Metrics row (2x2 grid in compact mode)
|
|
270
|
+
if self.compact:
|
|
271
|
+
# Compact: 2 metrics per row
|
|
272
|
+
for i in range(0, len(self._metrics), 2):
|
|
273
|
+
row_metrics = self._metrics[i : i + 2]
|
|
274
|
+
for j, metric in enumerate(row_metrics):
|
|
275
|
+
content.append(self._render_metric(metric))
|
|
276
|
+
if j < len(row_metrics) - 1:
|
|
277
|
+
content.append(" ")
|
|
278
|
+
content.append("\n")
|
|
279
|
+
else:
|
|
280
|
+
# Full: All metrics visible
|
|
281
|
+
for metric in self._metrics:
|
|
282
|
+
content.append(self._render_metric(metric))
|
|
283
|
+
content.append("\n")
|
|
284
|
+
|
|
285
|
+
# Active analysis section
|
|
286
|
+
if self.active_task:
|
|
287
|
+
content.append("\n")
|
|
288
|
+
|
|
289
|
+
# File being analyzed
|
|
290
|
+
spinner = self._get_analysis_spinner()
|
|
291
|
+
content.append(f"{spinner} ", style="bold #3b82f6")
|
|
292
|
+
content.append("Active Analysis: ", style="#a1a1aa")
|
|
293
|
+
|
|
294
|
+
# Truncate path if too long
|
|
295
|
+
path = self.active_task.file_path
|
|
296
|
+
if len(path) > 35:
|
|
297
|
+
path = "..." + path[-32:]
|
|
298
|
+
content.append(path, style="bold #e2e8f0")
|
|
299
|
+
content.append("\n")
|
|
300
|
+
|
|
301
|
+
# Description
|
|
302
|
+
content.append(" ├─ ", style="#3f3f46")
|
|
303
|
+
content.append(self.active_task.description, style="#a1a1aa")
|
|
304
|
+
content.append("\n")
|
|
305
|
+
|
|
306
|
+
# Progress bar
|
|
307
|
+
progress_pct = int(self.active_task.progress * 100)
|
|
308
|
+
bar_width = 30
|
|
309
|
+
filled = int(self.active_task.progress * bar_width)
|
|
310
|
+
|
|
311
|
+
content.append(" └─ [", style="#3f3f46")
|
|
312
|
+
content.append("█" * filled, style="bold #3b82f6")
|
|
313
|
+
content.append("░" * (bar_width - filled), style="#27272a")
|
|
314
|
+
content.append(f"] {progress_pct}%", style="#3f3f46")
|
|
315
|
+
|
|
316
|
+
return Panel(
|
|
317
|
+
content,
|
|
318
|
+
title=f"[bold #3b82f6]{self.title}[/]",
|
|
319
|
+
border_style="#3f3f46",
|
|
320
|
+
padding=(0, 1),
|
|
321
|
+
)
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SuperQode Resizable Sidebar - Draggable and Keyboard Resizing.
|
|
3
|
+
|
|
4
|
+
Provides a resizable sidebar container with:
|
|
5
|
+
- Draggable divider for mouse resize
|
|
6
|
+
- Keyboard shortcuts (Ctrl+[ / Ctrl+]) for resize
|
|
7
|
+
- Min/max width constraints
|
|
8
|
+
- Smooth resize animation
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from superqode.widgets.resizable_sidebar import (
|
|
12
|
+
ResizableDivider, ResizableSidebarContainer
|
|
13
|
+
)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Callable, Optional
|
|
19
|
+
|
|
20
|
+
from textual.widgets import Static
|
|
21
|
+
from textual.containers import Container, Horizontal
|
|
22
|
+
from textual.reactive import reactive
|
|
23
|
+
from textual.events import MouseDown, MouseMove, MouseUp
|
|
24
|
+
from textual.binding import Binding
|
|
25
|
+
from textual.message import Message
|
|
26
|
+
|
|
27
|
+
from rich.text import Text
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ============================================================================
|
|
31
|
+
# DESIGN SYSTEM
|
|
32
|
+
# ============================================================================
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from superqode.design_system import COLORS as SQ_COLORS, GRADIENT_PURPLE
|
|
36
|
+
except ImportError:
|
|
37
|
+
|
|
38
|
+
class SQ_COLORS:
|
|
39
|
+
primary = "#7c3aed"
|
|
40
|
+
primary_light = "#a855f7"
|
|
41
|
+
text_primary = "#fafafa"
|
|
42
|
+
text_secondary = "#e4e4e7"
|
|
43
|
+
text_muted = "#a1a1aa"
|
|
44
|
+
text_dim = "#71717a"
|
|
45
|
+
text_ghost = "#52525b"
|
|
46
|
+
border_subtle = "#1a1a1a"
|
|
47
|
+
border_default = "#27272a"
|
|
48
|
+
|
|
49
|
+
GRADIENT_PURPLE = ["#6d28d9", "#7c3aed", "#8b5cf6", "#a855f7"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ============================================================================
|
|
53
|
+
# RESIZABLE DIVIDER
|
|
54
|
+
# ============================================================================
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ResizableDivider(Static):
|
|
58
|
+
"""
|
|
59
|
+
Draggable divider for resizing adjacent panels.
|
|
60
|
+
|
|
61
|
+
SuperQode style: Minimal, purple highlight on hover/drag.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
DEFAULT_CSS = """
|
|
65
|
+
ResizableDivider {
|
|
66
|
+
width: 1;
|
|
67
|
+
height: 100%;
|
|
68
|
+
background: #1a1a1a;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
ResizableDivider:hover {
|
|
72
|
+
background: #7c3aed;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
ResizableDivider.dragging {
|
|
76
|
+
background: #a855f7;
|
|
77
|
+
}
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
# Messages
|
|
81
|
+
class Resized(Message):
|
|
82
|
+
"""Posted when divider is dragged."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, delta_x: int, screen_x: int) -> None:
|
|
85
|
+
self.delta_x = delta_x
|
|
86
|
+
self.screen_x = screen_x
|
|
87
|
+
super().__init__()
|
|
88
|
+
|
|
89
|
+
class ResizeStart(Message):
|
|
90
|
+
"""Posted when resize starts."""
|
|
91
|
+
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
class ResizeEnd(Message):
|
|
95
|
+
"""Posted when resize ends."""
|
|
96
|
+
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# State
|
|
100
|
+
dragging: reactive[bool] = reactive(False)
|
|
101
|
+
|
|
102
|
+
def __init__(self, **kwargs):
|
|
103
|
+
super().__init__("", **kwargs)
|
|
104
|
+
self._start_x: int = 0
|
|
105
|
+
self._last_x: int = 0
|
|
106
|
+
|
|
107
|
+
def watch_dragging(self, dragging: bool) -> None:
|
|
108
|
+
"""Update visual state when dragging changes."""
|
|
109
|
+
if dragging:
|
|
110
|
+
self.add_class("dragging")
|
|
111
|
+
else:
|
|
112
|
+
self.remove_class("dragging")
|
|
113
|
+
|
|
114
|
+
def on_mouse_down(self, event: MouseDown) -> None:
|
|
115
|
+
"""Start dragging."""
|
|
116
|
+
self.dragging = True
|
|
117
|
+
self._start_x = event.screen_x
|
|
118
|
+
self._last_x = event.screen_x
|
|
119
|
+
self.capture_mouse()
|
|
120
|
+
self.post_message(self.ResizeStart())
|
|
121
|
+
event.stop()
|
|
122
|
+
|
|
123
|
+
def on_mouse_move(self, event: MouseMove) -> None:
|
|
124
|
+
"""Handle drag movement."""
|
|
125
|
+
if self.dragging:
|
|
126
|
+
delta = event.screen_x - self._last_x
|
|
127
|
+
if delta != 0:
|
|
128
|
+
self.post_message(self.Resized(delta, event.screen_x))
|
|
129
|
+
self._last_x = event.screen_x
|
|
130
|
+
event.stop()
|
|
131
|
+
|
|
132
|
+
def on_mouse_up(self, event: MouseUp) -> None:
|
|
133
|
+
"""Stop dragging."""
|
|
134
|
+
if self.dragging:
|
|
135
|
+
self.dragging = False
|
|
136
|
+
self.release_mouse()
|
|
137
|
+
self.post_message(self.ResizeEnd())
|
|
138
|
+
event.stop()
|
|
139
|
+
|
|
140
|
+
def render(self) -> Text:
|
|
141
|
+
"""Render the divider."""
|
|
142
|
+
# Just a vertical line character
|
|
143
|
+
return Text("│", style=SQ_COLORS.primary if self.dragging else SQ_COLORS.border_subtle)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ============================================================================
|
|
147
|
+
# RESIZABLE SIDEBAR CONTAINER
|
|
148
|
+
# ============================================================================
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class ResizableSidebarContainer(Container):
|
|
152
|
+
"""
|
|
153
|
+
Container that wraps a sidebar and provides resize functionality.
|
|
154
|
+
|
|
155
|
+
Features:
|
|
156
|
+
- Drag-to-resize with ResizableDivider
|
|
157
|
+
- Keyboard shortcuts (Ctrl+[ / Ctrl+])
|
|
158
|
+
- Min/max width constraints
|
|
159
|
+
- Collapse/expand toggle
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
DEFAULT_CSS = """
|
|
163
|
+
ResizableSidebarContainer {
|
|
164
|
+
height: 100%;
|
|
165
|
+
layout: horizontal;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
ResizableSidebarContainer #rsb-sidebar {
|
|
169
|
+
height: 100%;
|
|
170
|
+
background: #000000;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
ResizableSidebarContainer #rsb-sidebar.collapsed {
|
|
174
|
+
width: 0;
|
|
175
|
+
display: none;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
ResizableSidebarContainer #rsb-divider {
|
|
179
|
+
width: 1;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
ResizableSidebarContainer #rsb-divider.hidden {
|
|
183
|
+
display: none;
|
|
184
|
+
}
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
BINDINGS = [
|
|
188
|
+
Binding("ctrl+[", "shrink_sidebar", "Shrink", show=False),
|
|
189
|
+
Binding("ctrl+]", "expand_sidebar", "Expand", show=False),
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
# State
|
|
193
|
+
sidebar_width: reactive[int] = reactive(80)
|
|
194
|
+
sidebar_visible: reactive[bool] = reactive(True)
|
|
195
|
+
|
|
196
|
+
# Config
|
|
197
|
+
min_width: int = 30
|
|
198
|
+
max_width: int = 150
|
|
199
|
+
resize_step: int = 10
|
|
200
|
+
|
|
201
|
+
def __init__(
|
|
202
|
+
self,
|
|
203
|
+
sidebar_content: Container,
|
|
204
|
+
min_width: int = 30,
|
|
205
|
+
max_width: int = 150,
|
|
206
|
+
initial_width: int = 80,
|
|
207
|
+
**kwargs,
|
|
208
|
+
):
|
|
209
|
+
super().__init__(**kwargs)
|
|
210
|
+
self._sidebar_content = sidebar_content
|
|
211
|
+
self.min_width = min_width
|
|
212
|
+
self.max_width = max_width
|
|
213
|
+
self.sidebar_width = initial_width
|
|
214
|
+
|
|
215
|
+
def compose(self):
|
|
216
|
+
"""Compose the resizable sidebar."""
|
|
217
|
+
# Sidebar container
|
|
218
|
+
with Container(id="rsb-sidebar"):
|
|
219
|
+
yield self._sidebar_content
|
|
220
|
+
|
|
221
|
+
# Divider
|
|
222
|
+
yield ResizableDivider(id="rsb-divider")
|
|
223
|
+
|
|
224
|
+
def on_mount(self) -> None:
|
|
225
|
+
"""Initialize sidebar width."""
|
|
226
|
+
self._update_sidebar_width()
|
|
227
|
+
|
|
228
|
+
def watch_sidebar_width(self, width: int) -> None:
|
|
229
|
+
"""Update sidebar width when changed."""
|
|
230
|
+
self._update_sidebar_width()
|
|
231
|
+
|
|
232
|
+
def watch_sidebar_visible(self, visible: bool) -> None:
|
|
233
|
+
"""Toggle sidebar visibility."""
|
|
234
|
+
try:
|
|
235
|
+
sidebar = self.query_one("#rsb-sidebar")
|
|
236
|
+
divider = self.query_one("#rsb-divider")
|
|
237
|
+
|
|
238
|
+
if visible:
|
|
239
|
+
sidebar.remove_class("collapsed")
|
|
240
|
+
divider.remove_class("hidden")
|
|
241
|
+
else:
|
|
242
|
+
sidebar.add_class("collapsed")
|
|
243
|
+
divider.add_class("hidden")
|
|
244
|
+
except Exception:
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
def _update_sidebar_width(self) -> None:
|
|
248
|
+
"""Apply the current width to the sidebar."""
|
|
249
|
+
try:
|
|
250
|
+
sidebar = self.query_one("#rsb-sidebar")
|
|
251
|
+
sidebar.styles.width = self.sidebar_width
|
|
252
|
+
except Exception:
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
def on_resizable_divider_resized(self, event: ResizableDivider.Resized) -> None:
|
|
256
|
+
"""Handle divider drag."""
|
|
257
|
+
new_width = self.sidebar_width + event.delta_x
|
|
258
|
+
new_width = max(self.min_width, min(self.max_width, new_width))
|
|
259
|
+
self.sidebar_width = new_width
|
|
260
|
+
|
|
261
|
+
def action_shrink_sidebar(self) -> None:
|
|
262
|
+
"""Shrink sidebar by step size."""
|
|
263
|
+
new_width = self.sidebar_width - self.resize_step
|
|
264
|
+
self.sidebar_width = max(self.min_width, new_width)
|
|
265
|
+
|
|
266
|
+
def action_expand_sidebar(self) -> None:
|
|
267
|
+
"""Expand sidebar by step size."""
|
|
268
|
+
new_width = self.sidebar_width + self.resize_step
|
|
269
|
+
self.sidebar_width = min(self.max_width, new_width)
|
|
270
|
+
|
|
271
|
+
def toggle_sidebar(self) -> None:
|
|
272
|
+
"""Toggle sidebar visibility."""
|
|
273
|
+
self.sidebar_visible = not self.sidebar_visible
|
|
274
|
+
|
|
275
|
+
def set_width(self, width: int) -> None:
|
|
276
|
+
"""Set sidebar width directly."""
|
|
277
|
+
self.sidebar_width = max(self.min_width, min(self.max_width, width))
|
|
278
|
+
|
|
279
|
+
def get_width(self) -> int:
|
|
280
|
+
"""Get current sidebar width."""
|
|
281
|
+
return self.sidebar_width
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ============================================================================
|
|
285
|
+
# SIDEBAR TAB BAR
|
|
286
|
+
# ============================================================================
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class SidebarTabBar(Static):
|
|
290
|
+
"""
|
|
291
|
+
Tab bar for switching between sidebar panels.
|
|
292
|
+
|
|
293
|
+
SuperQode style: Minimal tabs with purple active indicator.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
DEFAULT_CSS = """
|
|
297
|
+
SidebarTabBar {
|
|
298
|
+
height: 2;
|
|
299
|
+
background: #0a0a0a;
|
|
300
|
+
border-bottom: solid #1a1a1a;
|
|
301
|
+
padding: 0;
|
|
302
|
+
}
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
class TabSelected(Message):
|
|
306
|
+
"""Posted when a tab is selected."""
|
|
307
|
+
|
|
308
|
+
def __init__(self, tab_id: str, index: int) -> None:
|
|
309
|
+
self.tab_id = tab_id
|
|
310
|
+
self.index = index
|
|
311
|
+
super().__init__()
|
|
312
|
+
|
|
313
|
+
active_tab: reactive[int] = reactive(0)
|
|
314
|
+
|
|
315
|
+
def __init__(self, tabs: list[str], **kwargs):
|
|
316
|
+
super().__init__("", **kwargs)
|
|
317
|
+
self._tabs = tabs
|
|
318
|
+
|
|
319
|
+
def watch_active_tab(self, index: int) -> None:
|
|
320
|
+
"""Update display when active tab changes."""
|
|
321
|
+
self.refresh()
|
|
322
|
+
if 0 <= index < len(self._tabs):
|
|
323
|
+
self.post_message(self.TabSelected(self._tabs[index].lower(), index))
|
|
324
|
+
|
|
325
|
+
def select_tab(self, index: int) -> None:
|
|
326
|
+
"""Select a tab by index."""
|
|
327
|
+
if 0 <= index < len(self._tabs):
|
|
328
|
+
self.active_tab = index
|
|
329
|
+
|
|
330
|
+
def select_tab_by_name(self, name: str) -> None:
|
|
331
|
+
"""Select a tab by name."""
|
|
332
|
+
name_lower = name.lower()
|
|
333
|
+
for i, tab in enumerate(self._tabs):
|
|
334
|
+
if tab.lower() == name_lower:
|
|
335
|
+
self.active_tab = i
|
|
336
|
+
break
|
|
337
|
+
|
|
338
|
+
def render(self) -> Text:
|
|
339
|
+
"""Render the tab bar."""
|
|
340
|
+
text = Text()
|
|
341
|
+
|
|
342
|
+
for i, tab in enumerate(self._tabs):
|
|
343
|
+
is_active = i == self.active_tab
|
|
344
|
+
|
|
345
|
+
# Tab separator
|
|
346
|
+
if i > 0:
|
|
347
|
+
text.append(" ", style="")
|
|
348
|
+
|
|
349
|
+
# Tab name (abbreviated for space)
|
|
350
|
+
short_name = tab[:3] if len(tab) > 4 else tab
|
|
351
|
+
|
|
352
|
+
if is_active:
|
|
353
|
+
text.append(f"[{short_name}]", style=f"bold {SQ_COLORS.primary}")
|
|
354
|
+
else:
|
|
355
|
+
text.append(f" {short_name} ", style=SQ_COLORS.text_dim)
|
|
356
|
+
|
|
357
|
+
return text
|
|
358
|
+
|
|
359
|
+
def on_click(self, event) -> None:
|
|
360
|
+
"""Handle click to select tab."""
|
|
361
|
+
# Calculate which tab was clicked based on x position
|
|
362
|
+
# Each tab is roughly 5 characters wide
|
|
363
|
+
tab_width = 5
|
|
364
|
+
index = event.x // tab_width
|
|
365
|
+
if 0 <= index < len(self._tabs):
|
|
366
|
+
self.active_tab = index
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ============================================================================
|
|
370
|
+
# EXPORTS
|
|
371
|
+
# ============================================================================
|
|
372
|
+
|
|
373
|
+
__all__ = [
|
|
374
|
+
"ResizableDivider",
|
|
375
|
+
"ResizableSidebarContainer",
|
|
376
|
+
"SidebarTabBar",
|
|
377
|
+
]
|