cct-cli 0.7.9.0__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.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"""CAT Model Center — Native Ollama & Local/Cloud AI Model Manager.
|
|
2
|
+
Creator: Kazi Zillani (CAT Platform).
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- Curated authentic models & dynamic local discovery
|
|
6
|
+
- Deep Hardware Analyzer integration (CPU, RAM, GPU VRAM, CUDA/ROCm/Metal)
|
|
7
|
+
- Model Compatibility Engine with 5 standard tiers:
|
|
8
|
+
🟢 Excellent, 🟢 Recommended, 🟡 Usable, 🟠 Heavy, 🔴 Not Recommended
|
|
9
|
+
- CAT Benchmark System integration with transparent source attribution
|
|
10
|
+
- Category filtering: All, Popular, Coding, Reasoning, Vision, Small, Large, Installed
|
|
11
|
+
- Memory footprint visualizer (Weights, KV-Cache, VRAM usage, System RAM offload, TPS)
|
|
12
|
+
- Full action controls: Download, Run/Select, Remove, Benchmark
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
import sys
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
import time
|
|
20
|
+
import threading
|
|
21
|
+
from typing import Optional, Dict, Any, List
|
|
22
|
+
|
|
23
|
+
TEXTUAL_AVAILABLE = True
|
|
24
|
+
try:
|
|
25
|
+
from textual.screen import Screen
|
|
26
|
+
from textual.containers import Vertical, Horizontal, ScrollableContainer
|
|
27
|
+
from textual.widgets import Static, Button, Input, Label
|
|
28
|
+
from textual.binding import Binding
|
|
29
|
+
except Exception:
|
|
30
|
+
TEXTUAL_AVAILABLE = False
|
|
31
|
+
|
|
32
|
+
if TEXTUAL_AVAILABLE:
|
|
33
|
+
try:
|
|
34
|
+
from ..ollama_catalog import all_models, search_models, get_model, categories as get_categories
|
|
35
|
+
from ..hardware_analyzer import HardwareAnalyzer
|
|
36
|
+
from ..compatibility_engine import ModelCompatibilityEngine
|
|
37
|
+
from ..benchmark_system import CATBenchmarkSystem
|
|
38
|
+
from ..ollama_download import (
|
|
39
|
+
installed_models, pull_model, delete_model,
|
|
40
|
+
estimate_minutes, is_ollama_running, ensure_ollama_running
|
|
41
|
+
)
|
|
42
|
+
from .. import config as cct_config
|
|
43
|
+
from ..providers import provider_manager as pm
|
|
44
|
+
except Exception:
|
|
45
|
+
all_models = search_models = get_model = get_categories = None
|
|
46
|
+
HardwareAnalyzer = ModelCompatibilityEngine = CATBenchmarkSystem = None
|
|
47
|
+
installed_models = pull_model = delete_model = None
|
|
48
|
+
estimate_minutes = is_ollama_running = ensure_ollama_running = None
|
|
49
|
+
cct_config = pm = None
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
from . import theme_css
|
|
53
|
+
except Exception:
|
|
54
|
+
theme_css = None
|
|
55
|
+
|
|
56
|
+
_CATEGORIES = ["all", "popular", "coding", "reasoning", "vision", "small", "large", "installed"]
|
|
57
|
+
|
|
58
|
+
def _hex(role):
|
|
59
|
+
if theme_css:
|
|
60
|
+
try:
|
|
61
|
+
return theme_css.current_hex(role)
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
return "#888888"
|
|
65
|
+
|
|
66
|
+
class _ModelRow(Horizontal):
|
|
67
|
+
def __init__(self, m: Dict[str, Any], compat: Any, installed: bool = False):
|
|
68
|
+
super().__init__(classes="ollama-row")
|
|
69
|
+
self.m = m
|
|
70
|
+
self.compat = compat
|
|
71
|
+
self.installed = installed
|
|
72
|
+
|
|
73
|
+
def compose(self):
|
|
74
|
+
m = self.m
|
|
75
|
+
inst_tag = "[✓]" if self.installed else "○"
|
|
76
|
+
# Model name & tag
|
|
77
|
+
yield Static(f"{inst_tag} {m['name']}", classes="ollama-name")
|
|
78
|
+
yield Static(str(m.get("params", "?")), classes="ollama-params")
|
|
79
|
+
yield Static(f"{m.get('size_gb', 0):.1f}G", classes="ollama-size")
|
|
80
|
+
|
|
81
|
+
# Compatibility tier badge
|
|
82
|
+
tier = getattr(self.compat, "tier", "USABLE")
|
|
83
|
+
if tier in ("EXCELLENT", "RECOMMENDED"):
|
|
84
|
+
badge_markup = f"[{_hex('success')}]● {getattr(self.compat, 'tier_label_ascii', '[GOOD]')}[/]"
|
|
85
|
+
elif tier == "USABLE":
|
|
86
|
+
badge_markup = f"[{_hex('warning')}]● {getattr(self.compat, 'tier_label_ascii', '[USABLE]')}[/]"
|
|
87
|
+
else:
|
|
88
|
+
badge_markup = f"[{_hex('error')}]● {getattr(self.compat, 'tier_label_ascii', '[HEAVY]')}[/]"
|
|
89
|
+
|
|
90
|
+
yield Static(badge_markup, classes="ollama-compat")
|
|
91
|
+
|
|
92
|
+
class OllamaPanel(Screen):
|
|
93
|
+
"""Dedicated CAT Model Center modal screen."""
|
|
94
|
+
CSS = """
|
|
95
|
+
OllamaPanel { align: center middle; background: $app-background 75%; }
|
|
96
|
+
#ollama-box {
|
|
97
|
+
width: 106; max-width: 98%; height: 46; max-height: 90%;
|
|
98
|
+
background: $surface; border: round $border; padding: 0;
|
|
99
|
+
layout: vertical;
|
|
100
|
+
}
|
|
101
|
+
#ollama-titlebar { height: 3; min-height: 3; padding: 0 2; border-bottom: solid $border; }
|
|
102
|
+
#ollama-title { text-style: bold; width: 1fr; }
|
|
103
|
+
#ollama-body { height: 1fr; min-height: 0; layout: horizontal; }
|
|
104
|
+
#ollama-left { width: 62; min-width: 48; height: 100%; border-right: solid $border; layout: vertical; }
|
|
105
|
+
#ollama-search { height: 3; min-height: 3; padding: 0 1; border-bottom: solid $border; }
|
|
106
|
+
#ollama-search Input { width: 1fr; height: 1; background: transparent; border: none; }
|
|
107
|
+
#ollama-filters { height: 3; min-height: 3; padding: 0 1; overflow-x: auto; }
|
|
108
|
+
#ollama-filters Button { margin-right: 1; }
|
|
109
|
+
#ollama-pcspec { height: auto; max-height: 4; padding: 1 1; border-bottom: solid $border; color: $text-faint; font-size: 85%; }
|
|
110
|
+
#ollama-list { height: 1fr; min-height: 0; overflow-y: auto; scrollbar-gutter: stable; }
|
|
111
|
+
.ollama-row { height: 1; min-height: 1; padding: 0 1; }
|
|
112
|
+
.ollama-row:hover { background: $accent 8%; }
|
|
113
|
+
.ollama-row.-selected { background: $accent 18%; color: $accent; }
|
|
114
|
+
.ollama-name { width: 28; height: 1; overflow: hidden; }
|
|
115
|
+
.ollama-params { width: 8; height: 1; color: $text-faint; text-align: center; }
|
|
116
|
+
.ollama-size { width: 8; height: 1; color: $text-faint; text-align: right; }
|
|
117
|
+
.ollama-compat { width: 16; height: 1; text-align: right; }
|
|
118
|
+
#ollama-right { width: 44; min-width: 36; height: 100%; padding: 1 2; overflow-y: auto; }
|
|
119
|
+
#ollama-detail-title { text-style: bold; color: $accent; height: 1; }
|
|
120
|
+
#ollama-detail-desc { color: $text; height: auto; padding: 1 0; }
|
|
121
|
+
#ollama-compat-card { height: auto; padding: 1 0; border-top: solid $border; }
|
|
122
|
+
#ollama-benchmark-card { height: auto; padding: 1 0; border-top: solid $border; }
|
|
123
|
+
#ollama-progress-wrap { height: 5; min-height: 5; padding: 1 0; display: none; }
|
|
124
|
+
#ollama-progress-wrap.show { display: block; }
|
|
125
|
+
#ollama-progress-bar { height: 1; color: $accent; }
|
|
126
|
+
#ollama-progress-text { height: 1; color: $text-faint; text-align: center; }
|
|
127
|
+
#ollama-progress-eta { height: 1; color: $warning; text-align: center; }
|
|
128
|
+
#ollama-actions { height: 3; min-height: 3; padding: 1 0; border-top: solid $border; }
|
|
129
|
+
#ollama-actions Button { margin-right: 1; }
|
|
130
|
+
#ollama-hint { height: 1; color: $text-faint; padding: 0 2; }
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
BINDINGS = [Binding("escape", "close", "Close")]
|
|
134
|
+
|
|
135
|
+
def __init__(self):
|
|
136
|
+
super().__init__()
|
|
137
|
+
self._catalog = []
|
|
138
|
+
self._installed = set()
|
|
139
|
+
self._filtered = []
|
|
140
|
+
self._selected = None
|
|
141
|
+
self._category = "all"
|
|
142
|
+
self._query = ""
|
|
143
|
+
self._hw = None
|
|
144
|
+
self._engine = None
|
|
145
|
+
self._bench = None
|
|
146
|
+
self._downloading = None
|
|
147
|
+
self._progress = {}
|
|
148
|
+
# Generation counter for the 8s self-rearming installed-
|
|
149
|
+
# models poll: opening the panel N times must not leave N
|
|
150
|
+
# chains polling after close. on_unmount bumps the token so
|
|
151
|
+
# stray chains stop re-arming.
|
|
152
|
+
self._poll_token = 0
|
|
153
|
+
|
|
154
|
+
def compose(self):
|
|
155
|
+
with Vertical(id="ollama-box"):
|
|
156
|
+
with Horizontal(id="ollama-titlebar"):
|
|
157
|
+
yield Static("⬢ CAT Model Center — Native Local AI & Model Platform", id="ollama-title")
|
|
158
|
+
yield Button("✕", id="ollama-close", classes="cct-popup-close")
|
|
159
|
+
with Horizontal(id="ollama-body"):
|
|
160
|
+
with Vertical(id="ollama-left"):
|
|
161
|
+
with Horizontal(id="ollama-search"):
|
|
162
|
+
yield Input(placeholder="🔍 Search models… (qwen, llama, coding, 3b)", id="ollama-search-input")
|
|
163
|
+
with Horizontal(id="ollama-filters"):
|
|
164
|
+
for cat in _CATEGORIES:
|
|
165
|
+
yield Button(cat.title(), id=f"ollama-cat-{cat}", classes="cct-btn cct-btn-sm")
|
|
166
|
+
yield Static("Hardware Analyzer: Profiling system…", id="ollama-pcspec")
|
|
167
|
+
yield ScrollableContainer(id="ollama-list")
|
|
168
|
+
with Vertical(id="ollama-right"):
|
|
169
|
+
yield Static("Select a model", id="ollama-detail-title")
|
|
170
|
+
yield Static("Explore curated and dynamic models. Hardware badges reflect live device compatibility.", id="ollama-detail-desc")
|
|
171
|
+
yield Static("", id="ollama-compat-card")
|
|
172
|
+
yield Static("", id="ollama-benchmark-card")
|
|
173
|
+
with Vertical(id="ollama-progress-wrap"):
|
|
174
|
+
yield Static("", id="ollama-progress-bar")
|
|
175
|
+
yield Static("", id="ollama-progress-text")
|
|
176
|
+
yield Static("", id="ollama-progress-eta")
|
|
177
|
+
with Horizontal(id="ollama-actions"):
|
|
178
|
+
yield Button("⬇ Download", id="ollama-download", variant="primary", classes="cct-btn cct-btn-sm")
|
|
179
|
+
yield Button("▶ Run", id="ollama-use", classes="cct-btn cct-btn-sm")
|
|
180
|
+
yield Button("🗑 Remove", id="ollama-remove", classes="cct-btn cct-btn-sm")
|
|
181
|
+
yield Button("⚡ Bench", id="ollama-bench", classes="cct-btn cct-btn-sm")
|
|
182
|
+
yield Static("Enter: run/select • Click: inspect • Esc: close", id="ollama-hint")
|
|
183
|
+
|
|
184
|
+
def on_mount(self):
|
|
185
|
+
self._init_systems()
|
|
186
|
+
self._load_catalog()
|
|
187
|
+
self.call_after_refresh(self._refresh_pcspec)
|
|
188
|
+
self.call_after_refresh(self._refresh_list)
|
|
189
|
+
self.call_after_refresh(self._poll_installed)
|
|
190
|
+
|
|
191
|
+
def _init_systems(self):
|
|
192
|
+
try:
|
|
193
|
+
if HardwareAnalyzer:
|
|
194
|
+
self._hw = HardwareAnalyzer.analyze()
|
|
195
|
+
if ModelCompatibilityEngine and self._hw:
|
|
196
|
+
self._engine = ModelCompatibilityEngine(hw=self._hw)
|
|
197
|
+
if CATBenchmarkSystem:
|
|
198
|
+
self._bench = CATBenchmarkSystem()
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
def _load_catalog(self):
|
|
203
|
+
try:
|
|
204
|
+
self._catalog = all_models(include_dynamic=True) if all_models else []
|
|
205
|
+
except Exception:
|
|
206
|
+
self._catalog = []
|
|
207
|
+
try:
|
|
208
|
+
self._installed = set(installed_models()) if installed_models else set()
|
|
209
|
+
except Exception:
|
|
210
|
+
self._installed = set()
|
|
211
|
+
self._filtered = list(self._catalog)
|
|
212
|
+
|
|
213
|
+
def _refresh_pcspec(self):
|
|
214
|
+
try:
|
|
215
|
+
if self._hw:
|
|
216
|
+
cpu_txt = f"{self._hw.cpu_model} ({self._hw.cpu_threads}T)"
|
|
217
|
+
ram_txt = f"{self._hw.ram_total_gb:.1f}GB RAM"
|
|
218
|
+
gpu_txt = f"{self._hw.gpu_name} ({self._hw.vram_gb:.1f}GB VRAM via {self._hw.gpu_backend})" if self._hw.has_gpu else "CPU Only"
|
|
219
|
+
tier_txt = self._hw.local_ai_tier.value
|
|
220
|
+
txt = f"⚡ Device Profile: {gpu_txt} • {ram_txt} • {cpu_txt} • Tier: {tier_txt}"
|
|
221
|
+
else:
|
|
222
|
+
txt = "⚡ Hardware profile unavailable"
|
|
223
|
+
self.query_one("#ollama-pcspec", Static).update(txt)
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
def on_unmount(self):
|
|
228
|
+
# Invalidate the poll chain: the in-flight callback (if
|
|
229
|
+
# any) will see a stale token and stop re-arming.
|
|
230
|
+
try:
|
|
231
|
+
self._poll_token = getattr(self, "_poll_token", 0) + 1
|
|
232
|
+
except Exception:
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
def _poll_installed(self):
|
|
236
|
+
token = getattr(self, "_poll_token", 0)
|
|
237
|
+
try:
|
|
238
|
+
if not self.is_running:
|
|
239
|
+
return
|
|
240
|
+
except Exception:
|
|
241
|
+
pass
|
|
242
|
+
try:
|
|
243
|
+
if installed_models:
|
|
244
|
+
self._installed = set(installed_models())
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
try:
|
|
248
|
+
if getattr(self, "_poll_token", 0) != token:
|
|
249
|
+
return
|
|
250
|
+
self.set_timer(8, self._poll_installed)
|
|
251
|
+
except Exception:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
def _refresh_list(self):
|
|
255
|
+
try:
|
|
256
|
+
cont = self.query_one("#ollama-list", ScrollableContainer)
|
|
257
|
+
cont.remove_children()
|
|
258
|
+
except Exception:
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
q = self._query.lower().strip()
|
|
262
|
+
cat = self._category.lower()
|
|
263
|
+
|
|
264
|
+
filtered = []
|
|
265
|
+
for m in self._catalog:
|
|
266
|
+
is_inst = m["name"] in self._installed
|
|
267
|
+
if cat == "installed" and not is_inst:
|
|
268
|
+
continue
|
|
269
|
+
if cat not in ("all", "installed"):
|
|
270
|
+
m_cats = [c.lower() for c in m.get("categories", [])]
|
|
271
|
+
if cat not in m_cats and cat not in m.get("caps", []) and cat != m.get("family"):
|
|
272
|
+
continue
|
|
273
|
+
if q:
|
|
274
|
+
n_match = q in m.get("name", "").lower()
|
|
275
|
+
d_match = q in m.get("desc", "").lower()
|
|
276
|
+
f_match = q in m.get("family", "").lower()
|
|
277
|
+
p_match = q in m.get("params", "").lower()
|
|
278
|
+
if not (n_match or d_match or f_match or p_match):
|
|
279
|
+
continue
|
|
280
|
+
filtered.append(m)
|
|
281
|
+
|
|
282
|
+
# Sort: Installed first, then higher compatibility, then smaller size
|
|
283
|
+
def _sort_key(item):
|
|
284
|
+
inst = item["name"] in self._installed
|
|
285
|
+
compat = self._engine.evaluate(item) if self._engine else None
|
|
286
|
+
tier_rank = {"EXCELLENT": 0, "RECOMMENDED": 1, "USABLE": 2, "HEAVY": 3, "NOT_RECOMMENDED": 4}
|
|
287
|
+
t_val = tier_rank.get(getattr(compat, "tier", "USABLE"), 2)
|
|
288
|
+
return (not inst, t_val, item.get("size_gb", 0))
|
|
289
|
+
|
|
290
|
+
filtered.sort(key=_sort_key)
|
|
291
|
+
self._filtered = filtered[:200]
|
|
292
|
+
|
|
293
|
+
for m in self._filtered:
|
|
294
|
+
compat = self._engine.evaluate(m) if self._engine else None
|
|
295
|
+
is_inst = m["name"] in self._installed
|
|
296
|
+
row = _ModelRow(m, compat=compat, installed=is_inst)
|
|
297
|
+
if self._selected and self._selected.get("name") == m["name"]:
|
|
298
|
+
row.add_class("-selected")
|
|
299
|
+
try:
|
|
300
|
+
cont.mount(row)
|
|
301
|
+
except Exception:
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
if not self._filtered:
|
|
305
|
+
try:
|
|
306
|
+
cont.mount(Static("No matching models found. Try another search.", classes="pc-empty-msg"))
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
def _show_detail(self, m: Dict[str, Any]):
|
|
311
|
+
self._selected = m
|
|
312
|
+
try:
|
|
313
|
+
name = m["name"]
|
|
314
|
+
self.query_one("#ollama-detail-title", Static).update(
|
|
315
|
+
f"{name} • {m.get('params', '?')} • {m.get('size_gb', 0):.1f}GB"
|
|
316
|
+
)
|
|
317
|
+
self.query_one("#ollama-detail-desc", Static).update(m.get("desc", ""))
|
|
318
|
+
|
|
319
|
+
# Compatibility Card
|
|
320
|
+
compat = self._engine.evaluate(m) if self._engine else None
|
|
321
|
+
if compat:
|
|
322
|
+
c_lines = [
|
|
323
|
+
f"[b]Device Compatibility: {compat.tier_label_ascii}[/b]",
|
|
324
|
+
f" Memory: {compat.total_memory_gb:.1f}GB (Weights: {compat.weights_gb:.1f}GB, KV: {compat.kv_cache_gb:.1f}GB)",
|
|
325
|
+
f" VRAM: {compat.vram_used_gb:.1f}GB • RAM Offload: {compat.ram_offload_gb:.1f}GB",
|
|
326
|
+
f" Speed: ~{compat.estimated_tps:.1f} tps • {compat.summary}",
|
|
327
|
+
]
|
|
328
|
+
self.query_one("#ollama-compat-card", Static).update("\n".join(c_lines))
|
|
329
|
+
else:
|
|
330
|
+
self.query_one("#ollama-compat-card", Static).update("")
|
|
331
|
+
|
|
332
|
+
# Benchmark Card
|
|
333
|
+
bench_score = self._bench.get_score_for_model(name) if self._bench else None
|
|
334
|
+
if bench_score:
|
|
335
|
+
src = bench_score.source.value if hasattr(bench_score.source, "value") else str(bench_score.source)
|
|
336
|
+
b_lines = [
|
|
337
|
+
f"[b]CAT Benchmark:[/] Score {bench_score.overall:.1f}/100 [{src}]",
|
|
338
|
+
f" Coding: {bench_score.coding:.1f} • Reason: {bench_score.reasoning:.1f} • Tools: {bench_score.tool_use:.1f}",
|
|
339
|
+
]
|
|
340
|
+
self.query_one("#ollama-benchmark-card", Static).update("\n".join(b_lines))
|
|
341
|
+
else:
|
|
342
|
+
self.query_one("#ollama-benchmark-card", Static).update(
|
|
343
|
+
"[dim]No CAT benchmark recorded. Click [Bench] to evaluate.[/dim]"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Buttons
|
|
347
|
+
is_inst = name in self._installed
|
|
348
|
+
dl_btn = self.query_one("#ollama-download", Button)
|
|
349
|
+
use_btn = self.query_one("#ollama-use", Button)
|
|
350
|
+
rm_btn = self.query_one("#ollama-remove", Button)
|
|
351
|
+
|
|
352
|
+
if is_inst:
|
|
353
|
+
dl_btn.label = "✓ Installed"
|
|
354
|
+
dl_btn.disabled = True
|
|
355
|
+
use_btn.disabled = False
|
|
356
|
+
rm_btn.disabled = False
|
|
357
|
+
else:
|
|
358
|
+
dl_btn.label = "⬇ Download"
|
|
359
|
+
dl_btn.disabled = False
|
|
360
|
+
use_btn.disabled = True
|
|
361
|
+
rm_btn.disabled = True
|
|
362
|
+
|
|
363
|
+
self._refresh_list()
|
|
364
|
+
except Exception:
|
|
365
|
+
pass
|
|
366
|
+
|
|
367
|
+
def _start_download(self):
|
|
368
|
+
if not self._selected or not pull_model:
|
|
369
|
+
return
|
|
370
|
+
name = self._selected["name"]
|
|
371
|
+
if name in self._installed or self._downloading:
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
if ensure_ollama_running:
|
|
375
|
+
ok, emsg = ensure_ollama_running(timeout=6, auto_start=True)
|
|
376
|
+
if not ok:
|
|
377
|
+
self._show_progress_error(f"Cannot start Ollama: {emsg}")
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
self._downloading = name
|
|
381
|
+
try:
|
|
382
|
+
wrap = self.query_one("#ollama-progress-wrap")
|
|
383
|
+
wrap.add_class("show")
|
|
384
|
+
self.query_one("#ollama-progress-bar", Static).update("⟳ Connecting…")
|
|
385
|
+
self.query_one("#ollama-progress-text", Static).update("0%")
|
|
386
|
+
dl_btn = self.query_one("#ollama-download", Button)
|
|
387
|
+
dl_btn.label = "⏳ Pulling…"
|
|
388
|
+
dl_btn.disabled = True
|
|
389
|
+
except Exception:
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
def on_prog(p):
|
|
393
|
+
def _upd():
|
|
394
|
+
try:
|
|
395
|
+
pct = p.get("percent")
|
|
396
|
+
st = p.get("status", "")
|
|
397
|
+
speed = p.get("speed")
|
|
398
|
+
bar_w = 26
|
|
399
|
+
if pct is not None:
|
|
400
|
+
filled = int(bar_w * pct / 100)
|
|
401
|
+
bar = "█" * filled + "░" * (bar_w - filled)
|
|
402
|
+
self.query_one("#ollama-progress-bar", Static).update(f"[{bar}]")
|
|
403
|
+
self.query_one("#ollama-progress-text", Static).update(f"{pct}% {st[:24]}")
|
|
404
|
+
else:
|
|
405
|
+
self.query_one("#ollama-progress-bar", Static).update(f"⟳ {st[:28]}")
|
|
406
|
+
if speed:
|
|
407
|
+
self.query_one("#ollama-progress-eta", Static).update(f"{speed/1024/1024:.1f} MB/s")
|
|
408
|
+
except Exception:
|
|
409
|
+
pass
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
self.app.call_from_thread(_upd)
|
|
413
|
+
except Exception:
|
|
414
|
+
_upd()
|
|
415
|
+
|
|
416
|
+
def on_done(ok, msg):
|
|
417
|
+
def _done():
|
|
418
|
+
try:
|
|
419
|
+
wrap = self.query_one("#ollama-progress-wrap")
|
|
420
|
+
if ok:
|
|
421
|
+
wrap.remove_class("show")
|
|
422
|
+
self._installed.add(name)
|
|
423
|
+
self._downloading = None
|
|
424
|
+
self.query_one("#ollama-download", Button).label = "✓ Installed"
|
|
425
|
+
self.query_one("#ollama-use", Button).disabled = False
|
|
426
|
+
self.query_one("#ollama-remove", Button).disabled = False
|
|
427
|
+
self._use_model(name)
|
|
428
|
+
else:
|
|
429
|
+
self._show_progress_error(f"Download failed: {msg}")
|
|
430
|
+
self._downloading = None
|
|
431
|
+
dl = self.query_one("#ollama-download", Button)
|
|
432
|
+
dl.label = "⬇ Retry"
|
|
433
|
+
dl.disabled = False
|
|
434
|
+
self._refresh_list()
|
|
435
|
+
except Exception:
|
|
436
|
+
pass
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
self.app.call_from_thread(_done)
|
|
440
|
+
except Exception:
|
|
441
|
+
_done()
|
|
442
|
+
|
|
443
|
+
try:
|
|
444
|
+
pull_model(name, on_progress=on_prog, on_done=on_done)
|
|
445
|
+
except Exception as e:
|
|
446
|
+
on_done(False, str(e))
|
|
447
|
+
|
|
448
|
+
def _show_progress_error(self, err_text: str):
|
|
449
|
+
try:
|
|
450
|
+
wrap = self.query_one("#ollama-progress-wrap")
|
|
451
|
+
wrap.add_class("show")
|
|
452
|
+
self.query_one("#ollama-progress-bar", Static).update(f"✗ Error")
|
|
453
|
+
self.query_one("#ollama-progress-text", Static).update(err_text[:36])
|
|
454
|
+
except Exception:
|
|
455
|
+
pass
|
|
456
|
+
|
|
457
|
+
def _remove_model(self):
|
|
458
|
+
if not self._selected or not delete_model:
|
|
459
|
+
return
|
|
460
|
+
name = self._selected["name"]
|
|
461
|
+
ok, msg = delete_model(name)
|
|
462
|
+
if ok:
|
|
463
|
+
self._installed.discard(name)
|
|
464
|
+
self._show_detail(self._selected)
|
|
465
|
+
self._refresh_list()
|
|
466
|
+
|
|
467
|
+
def _run_benchmark(self):
|
|
468
|
+
if not self._selected or not self._bench:
|
|
469
|
+
return
|
|
470
|
+
name = self._selected["name"]
|
|
471
|
+
score = self._bench.run_full_benchmark(name)
|
|
472
|
+
self._show_detail(self._selected)
|
|
473
|
+
|
|
474
|
+
def _use_model(self, name: str):
|
|
475
|
+
try:
|
|
476
|
+
if cct_config:
|
|
477
|
+
cc = cct_config.get_config()
|
|
478
|
+
cc.default_ai_provider = "ollama"
|
|
479
|
+
cc.default_model = name
|
|
480
|
+
cct_config.save_config(cc)
|
|
481
|
+
|
|
482
|
+
if pm:
|
|
483
|
+
cfg = pm.load_config()
|
|
484
|
+
cfg["provider"] = "ollama"
|
|
485
|
+
cfg["model"] = name
|
|
486
|
+
pm.save_config(cfg)
|
|
487
|
+
|
|
488
|
+
try:
|
|
489
|
+
self.app._system_note(f"✓ CAT Model activated: {name}")
|
|
490
|
+
except Exception:
|
|
491
|
+
pass
|
|
492
|
+
self.dismiss(name)
|
|
493
|
+
except Exception:
|
|
494
|
+
self.dismiss(name)
|
|
495
|
+
|
|
496
|
+
def on_button_pressed(self, event):
|
|
497
|
+
bid = event.button.id or ""
|
|
498
|
+
if bid == "ollama-close":
|
|
499
|
+
self.dismiss(None)
|
|
500
|
+
elif bid.startswith("ollama-cat-"):
|
|
501
|
+
self._category = bid.replace("ollama-cat-", "")
|
|
502
|
+
for b in self.query(Button):
|
|
503
|
+
if b.id and b.id.startswith("ollama-cat-"):
|
|
504
|
+
b.variant = "primary" if b.id == bid else "default"
|
|
505
|
+
self._refresh_list()
|
|
506
|
+
elif bid == "ollama-download":
|
|
507
|
+
self._start_download()
|
|
508
|
+
elif bid == "ollama-use":
|
|
509
|
+
if self._selected:
|
|
510
|
+
self._use_model(self._selected["name"])
|
|
511
|
+
elif bid == "ollama-remove":
|
|
512
|
+
self._remove_model()
|
|
513
|
+
elif bid == "ollama-bench":
|
|
514
|
+
self._run_benchmark()
|
|
515
|
+
|
|
516
|
+
def on_input_changed(self, event):
|
|
517
|
+
if event.input.id == "ollama-search-input":
|
|
518
|
+
self._query = event.value
|
|
519
|
+
self._refresh_list()
|
|
520
|
+
|
|
521
|
+
def on_click(self, event):
|
|
522
|
+
w = getattr(event, "widget", None)
|
|
523
|
+
cur = w
|
|
524
|
+
for _ in range(4):
|
|
525
|
+
if cur is None:
|
|
526
|
+
break
|
|
527
|
+
if isinstance(cur, _ModelRow):
|
|
528
|
+
self._show_detail(cur.m)
|
|
529
|
+
break
|
|
530
|
+
cur = getattr(cur, "parent", None)
|
|
531
|
+
|
|
532
|
+
def action_close(self):
|
|
533
|
+
self.dismiss(None)
|
|
534
|
+
|
|
535
|
+
else:
|
|
536
|
+
OllamaPanel = None
|