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,489 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Vision — terminal vision panel (v0.8.a).
|
|
3
|
+
|
|
4
|
+
Standalone CAT feature.
|
|
5
|
+
Shows image-attach workflow, annotation, and chat.
|
|
6
|
+
|
|
7
|
+
Responsive design: adapts to narrow terminals via scroll + stacked rows.
|
|
8
|
+
Circle uses WHITE CIRCLE (U+25CB) which renders at single-cell width in
|
|
9
|
+
Textual, unlike the emoji circle (U+2B55) which measures as double-width
|
|
10
|
+
and breaks button layout.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
print("This is a library file and is not meant to be run directly.")
|
|
15
|
+
import sys
|
|
16
|
+
sys.exit(1)
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
TEXTUAL_AVAILABLE = True
|
|
21
|
+
try:
|
|
22
|
+
from textual.screen import Screen
|
|
23
|
+
from textual.containers import Vertical, Horizontal, VerticalScroll
|
|
24
|
+
from textual.widgets import Static, Button, Input, LoadingIndicator
|
|
25
|
+
from textual.binding import Binding
|
|
26
|
+
except Exception:
|
|
27
|
+
TEXTUAL_AVAILABLE = False
|
|
28
|
+
|
|
29
|
+
if TEXTUAL_AVAILABLE:
|
|
30
|
+
class VisionPanel(Screen):
|
|
31
|
+
CSS = """
|
|
32
|
+
VisionPanel { align: center middle; background: $app-background 60%; }
|
|
33
|
+
|
|
34
|
+
#vision-box {
|
|
35
|
+
width: 86; height: auto; max-height: 44;
|
|
36
|
+
background: $surface; border: round $border;
|
|
37
|
+
padding: 1 1;
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
layout: vertical;
|
|
40
|
+
}
|
|
41
|
+
/* Title + live indicator row */
|
|
42
|
+
#vision-title-row { height: 1; layout: horizontal; }
|
|
43
|
+
#vision-title { width: auto; text-style: bold; color: $accent; }
|
|
44
|
+
#vision-live { width: 1fr; text-align: right; color: $text-muted; }
|
|
45
|
+
|
|
46
|
+
#vision-subtitle { height: 1; color: $text-faint; padding-bottom: 1; }
|
|
47
|
+
#vision-disabled { height: auto; padding: 2 2; align: center middle; }
|
|
48
|
+
.vision-disabled-msg { color: $warning; text-style: bold; height: auto; text-align: center; padding-bottom: 1; }
|
|
49
|
+
|
|
50
|
+
/* Preview area - flexible, scrollable */
|
|
51
|
+
#vision-preview-wrap {
|
|
52
|
+
height: 1fr; min-height: 8; max-height: 14;
|
|
53
|
+
border: solid $border; background: $app-background;
|
|
54
|
+
overflow-y: auto; overflow-x: hidden;
|
|
55
|
+
scrollbar-gutter: stable;
|
|
56
|
+
scrollbar-size: 1 1;
|
|
57
|
+
padding: 1;
|
|
58
|
+
}
|
|
59
|
+
#vision-preview { height: auto; color: $text; text-align: center; }
|
|
60
|
+
#vision-stats { height: 1; color: $text-faint; text-align: right; padding-right: 1; }
|
|
61
|
+
|
|
62
|
+
/* Toolbars - horizontal scroll on narrow, no clipping */
|
|
63
|
+
.vision-toolbar {
|
|
64
|
+
height: auto; min-height: 3;
|
|
65
|
+
layout: horizontal;
|
|
66
|
+
overflow-x: auto; overflow-y: hidden;
|
|
67
|
+
scrollbar-size: 1 1;
|
|
68
|
+
scrollbar-gutter: stable;
|
|
69
|
+
padding: 1 0 0 0;
|
|
70
|
+
}
|
|
71
|
+
.vision-toolbar Button {
|
|
72
|
+
margin-right: 1; margin-bottom: 1;
|
|
73
|
+
min-width: 10; height: 3; min-height: 3;
|
|
74
|
+
}
|
|
75
|
+
.vision-toolbar Button.is-active {
|
|
76
|
+
border: round $accent; color: $accent; text-style: bold;
|
|
77
|
+
background: $accent 14%;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Chat */
|
|
81
|
+
#vision-chat-wrap {
|
|
82
|
+
height: 1fr; min-height: 8;
|
|
83
|
+
border: solid $border; padding: 1 1 0 1;
|
|
84
|
+
overflow: hidden; layout: vertical;
|
|
85
|
+
}
|
|
86
|
+
#vision-chat-log {
|
|
87
|
+
height: 1fr; overflow-y: auto; overflow-x: hidden;
|
|
88
|
+
scrollbar-gutter: stable; scrollbar-size: 1 1;
|
|
89
|
+
padding-right: 1; color: $text;
|
|
90
|
+
}
|
|
91
|
+
#vision-chat-empty { color: $text-faint; height: auto; padding: 1 0; }
|
|
92
|
+
.vision-msg-user { color: $accent; padding: 1 0 0 0; }
|
|
93
|
+
.vision-msg-cat { color: $text; padding: 1 0 0 0; }
|
|
94
|
+
|
|
95
|
+
#vision-input-row { height: 3; layout: horizontal; margin-top: 1; padding-bottom: 1; }
|
|
96
|
+
#vision-input-row Input { width: 1fr; margin-right: 1; }
|
|
97
|
+
#vision-input-row Button { min-width: 12; }
|
|
98
|
+
|
|
99
|
+
#vision-actions {
|
|
100
|
+
height: auto; min-height: 3;
|
|
101
|
+
layout: horizontal; overflow-x: auto;
|
|
102
|
+
scrollbar-size: 1 1; scrollbar-gutter: stable;
|
|
103
|
+
padding-top: 1;
|
|
104
|
+
border-top: solid $border;
|
|
105
|
+
}
|
|
106
|
+
#vision-actions Button { margin-right: 1; min-width: 16; }
|
|
107
|
+
|
|
108
|
+
#vision-hint { color: $text-faint; height: 1; padding-top: 1; text-align: center; }
|
|
109
|
+
|
|
110
|
+
/* Responsive narrow fallback */
|
|
111
|
+
.cct-compact #vision-box { width: 96; height: auto; }
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
BINDINGS = [
|
|
115
|
+
Binding("escape", "cancel", "Cancel"),
|
|
116
|
+
Binding("ctrl+enter", "send", "Send"),
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
def compose(self):
|
|
120
|
+
# Extension guard: Vision is an installable extension
|
|
121
|
+
try:
|
|
122
|
+
from .. import extensions as _ext
|
|
123
|
+
if not _ext.is_enabled("vision"):
|
|
124
|
+
with Vertical(id="vision-box"):
|
|
125
|
+
with Horizontal(id="vision-title-row"):
|
|
126
|
+
yield Static("CAT Vision", id="vision-title")
|
|
127
|
+
yield Static("OFF", id="vision-live")
|
|
128
|
+
yield Static("CAT Vision is disabled — enable it in Extensions to use vision.", id="vision-subtitle")
|
|
129
|
+
with Vertical(id="vision-disabled"):
|
|
130
|
+
yield Static("👁 CAT Vision is currently disabled.", classes="vision-disabled-msg")
|
|
131
|
+
yield Button("Open Extensions", id="vision-open-ext", variant="primary")
|
|
132
|
+
yield Button("Close", id="vision-close2")
|
|
133
|
+
return
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
with Vertical(id="vision-box"):
|
|
137
|
+
with Horizontal(id="vision-title-row"):
|
|
138
|
+
yield Static("CAT Vision", id="vision-title")
|
|
139
|
+
yield Static("OFF", id="vision-live")
|
|
140
|
+
yield Static("Share -> Annotate -> Chat -> Analyze -> Fix -> Verify", id="vision-subtitle")
|
|
141
|
+
# Preview
|
|
142
|
+
with Vertical(id="vision-preview-wrap"):
|
|
143
|
+
yield Static(
|
|
144
|
+
"No capture yet.\n"
|
|
145
|
+
"Attach an image or start a session below.\n"
|
|
146
|
+
"Your screen is never captured without explicit action.",
|
|
147
|
+
id="vision-preview"
|
|
148
|
+
)
|
|
149
|
+
yield Static("0 frames · 0 annotations", id="vision-stats")
|
|
150
|
+
# Toolbar row 1: capture controls
|
|
151
|
+
with Horizontal(classes="vision-toolbar", id="vision-tb-capture"):
|
|
152
|
+
yield Button("Start", id="vision-start", variant="success", classes="cct-btn-sm")
|
|
153
|
+
yield Button("Pause", id="vision-pause", classes="cct-btn-sm")
|
|
154
|
+
yield Button("Resume", id="vision-resume", classes="cct-btn-sm")
|
|
155
|
+
yield Button("Stop", id="vision-stop", variant="error", classes="cct-btn-sm")
|
|
156
|
+
yield Button("Attach Image", id="vision-attach", classes="cct-btn-sm")
|
|
157
|
+
yield Button("Clear", id="vision-clear", classes="cct-btn-sm")
|
|
158
|
+
# Toolbar row 2: annotation tools - WHITE CIRCLE fixes rendering
|
|
159
|
+
with Horizontal(classes="vision-toolbar", id="vision-tb-tools"):
|
|
160
|
+
yield Button("Pen", id="vision-pen", classes="cct-btn-sm")
|
|
161
|
+
yield Button("Highlight", id="vision-highlight", classes="cct-btn-sm")
|
|
162
|
+
yield Button("O Circle", id="vision-circle", classes="cct-btn-sm")
|
|
163
|
+
yield Button("Rect", id="vision-rect", classes="cct-btn-sm")
|
|
164
|
+
yield Button("Arrow", id="vision-arrow", classes="cct-btn-sm")
|
|
165
|
+
yield Button("Text", id="vision-text", classes="cct-btn-sm")
|
|
166
|
+
yield Button("Undo", id="vision-undo", classes="cct-btn-sm")
|
|
167
|
+
yield Button("Redo", id="vision-redo", classes="cct-btn-sm")
|
|
168
|
+
# Chat
|
|
169
|
+
with Vertical(id="vision-chat-wrap"):
|
|
170
|
+
with VerticalScroll(id="vision-chat-log"):
|
|
171
|
+
yield Static("Ask CAT about the current capture. Attach an image, annotate with tools above, then chat.", id="vision-chat-empty", classes="cct-text-faint")
|
|
172
|
+
with Horizontal(id="vision-input-row"):
|
|
173
|
+
yield Input(placeholder="Describe the problem... (e.g. this button glitches)", id="vision-input")
|
|
174
|
+
yield Button("Send", id="vision-send", variant="primary", classes="cct-btn-sm")
|
|
175
|
+
# Actions
|
|
176
|
+
with Horizontal(id="vision-actions"):
|
|
177
|
+
yield Button("Analyze Selection", id="vision-analyze-sel", variant="primary", classes="cct-btn-sm")
|
|
178
|
+
yield Button("Analyze Screen", id="vision-analyze-all", classes="cct-btn-sm")
|
|
179
|
+
yield Button("Fix This", id="vision-fix", classes="cct-btn-sm")
|
|
180
|
+
yield Static("Esc to close · Enter to send · White circle fixes emoji width bug", id="vision-hint")
|
|
181
|
+
|
|
182
|
+
def on_mount(self):
|
|
183
|
+
self._active_tool = "pen"
|
|
184
|
+
self._session_live = False
|
|
185
|
+
self._frame_count = 0
|
|
186
|
+
self._ann_count = 0
|
|
187
|
+
self._chat_turns = 0
|
|
188
|
+
self._attached_image = None # path
|
|
189
|
+
self._highlight_tool("pen")
|
|
190
|
+
self._refresh_chrome()
|
|
191
|
+
# compact check (called once, no need for resize loop in panel)
|
|
192
|
+
try:
|
|
193
|
+
if self.size.width < 72 or self.size.height < 32:
|
|
194
|
+
self.query_one("#vision-box").add_class("cct-compact")
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
def _highlight_tool(self, tool_id):
|
|
199
|
+
# remove old, add new
|
|
200
|
+
for bid in ("vision-pen","vision-highlight","vision-circle","vision-rect","vision-arrow","vision-text"):
|
|
201
|
+
try:
|
|
202
|
+
self.query_one(f"#{bid}", Button).remove_class("is-active")
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
try:
|
|
206
|
+
self.query_one(f"#vision-{tool_id}", Button).add_class("is-active")
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
self._active_tool = tool_id
|
|
210
|
+
|
|
211
|
+
def _refresh_chrome(self):
|
|
212
|
+
try:
|
|
213
|
+
live = self.query_one("#vision-live", Static)
|
|
214
|
+
if self._session_live:
|
|
215
|
+
live.update("[b green]● LIVE[/]")
|
|
216
|
+
else:
|
|
217
|
+
live.update("[dim]○ OFF[/]")
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
try:
|
|
221
|
+
self.query_one("#vision-stats", Static).update(f"{self._frame_count} frames · {self._ann_count} annotations")
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
def _set_preview(self, text):
|
|
226
|
+
try:
|
|
227
|
+
self.query_one("#vision-preview", Static).update(text)
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
def _push_chat(self, who, text):
|
|
232
|
+
# remove empty hint after first message
|
|
233
|
+
try:
|
|
234
|
+
empty = self.query_one("#vision-chat-empty", Static)
|
|
235
|
+
if empty:
|
|
236
|
+
empty.remove()
|
|
237
|
+
except Exception:
|
|
238
|
+
pass
|
|
239
|
+
try:
|
|
240
|
+
log = self.query_one("#vision-chat-log", VerticalScroll)
|
|
241
|
+
cls = "vision-msg-user" if who == "you" else "vision-msg-cat"
|
|
242
|
+
safe = text.replace("[", "\\[").replace("]", "\\]")
|
|
243
|
+
# Use markup: bold who, plain text
|
|
244
|
+
if who == "you":
|
|
245
|
+
markup = f"[b #7aa2f7]You:[/] {safe}"
|
|
246
|
+
elif who == "cat":
|
|
247
|
+
markup = f"[b #9ece6a]CAT:[/] {safe}"
|
|
248
|
+
else:
|
|
249
|
+
markup = f"[dim]{safe}[/]"
|
|
250
|
+
w = Static(markup, classes=cls)
|
|
251
|
+
log.mount(w)
|
|
252
|
+
log.scroll_end(animate=False)
|
|
253
|
+
self._chat_turns += 1
|
|
254
|
+
except Exception:
|
|
255
|
+
pass
|
|
256
|
+
|
|
257
|
+
def _update_status(self, msg):
|
|
258
|
+
# legacy helper for tests that call it
|
|
259
|
+
self._set_preview(msg)
|
|
260
|
+
|
|
261
|
+
# ---- helpers for real attachment ----
|
|
262
|
+
def _pick_image_via_dialog(self):
|
|
263
|
+
# Use native file picker where available (desktop), else show input
|
|
264
|
+
from textual.widgets import Input as _Input
|
|
265
|
+
# Try tkinter first (desktop) - non-blocking fallback to Input
|
|
266
|
+
path = None
|
|
267
|
+
try:
|
|
268
|
+
import tkinter as tk
|
|
269
|
+
from tkinter import filedialog
|
|
270
|
+
root = tk.Tk()
|
|
271
|
+
root.withdraw()
|
|
272
|
+
root.attributes("-topmost", True)
|
|
273
|
+
p = filedialog.askopenfilename(
|
|
274
|
+
title="Attach image for CAT Vision",
|
|
275
|
+
filetypes=[("Images","*.png *.jpg *.jpeg *.webp *.gif *.bmp"),("All","*.*")],
|
|
276
|
+
)
|
|
277
|
+
try:
|
|
278
|
+
root.destroy()
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
281
|
+
if p:
|
|
282
|
+
path = p
|
|
283
|
+
except Exception:
|
|
284
|
+
path = None
|
|
285
|
+
return path
|
|
286
|
+
|
|
287
|
+
def _attach_image(self, path):
|
|
288
|
+
if not path or not os.path.isfile(path):
|
|
289
|
+
self._push_chat("sys", f"Attach failed: not a file: {path}")
|
|
290
|
+
return
|
|
291
|
+
self._attached_image = path
|
|
292
|
+
self._frame_count += 1
|
|
293
|
+
self._refresh_chrome()
|
|
294
|
+
name = os.path.basename(path)
|
|
295
|
+
try:
|
|
296
|
+
size = os.path.getsize(path)
|
|
297
|
+
kb = f"{size/1024:.1f} KB" if size < 1024*1024 else f"{size/1024/1024:.1f} MB"
|
|
298
|
+
except Exception:
|
|
299
|
+
kb = "?"
|
|
300
|
+
self._set_preview(f"Attached: {name} ({kb})\n{path}\n\nAnnotation: {self._active_tool} · add a label in chat\nReady to analyze.")
|
|
301
|
+
self._push_chat("sys", f"Attached image: {name}")
|
|
302
|
+
|
|
303
|
+
def _do_analyze(self, selection_only=True):
|
|
304
|
+
# If no image attached, prompt
|
|
305
|
+
if not self._attached_image:
|
|
306
|
+
self._push_chat("sys", "Attach an image first (Attach Image button).")
|
|
307
|
+
return
|
|
308
|
+
prompt = ""
|
|
309
|
+
try:
|
|
310
|
+
inp = self.query_one("#vision-input", Input)
|
|
311
|
+
prompt = inp.value.strip()
|
|
312
|
+
except Exception:
|
|
313
|
+
prompt = ""
|
|
314
|
+
if not prompt:
|
|
315
|
+
prompt = "Analyze this. What do you see and what is the likely issue?"
|
|
316
|
+
# push user turn
|
|
317
|
+
self._push_chat("you", prompt)
|
|
318
|
+
try:
|
|
319
|
+
self.query_one("#vision-input", Input).value = ""
|
|
320
|
+
except Exception:
|
|
321
|
+
pass
|
|
322
|
+
# Show thinking
|
|
323
|
+
self._push_chat("cat", "Analyzing with CAT Vision...")
|
|
324
|
+
|
|
325
|
+
# Real call off-thread: normalize + classify + vision model via calc_terminal.vision
|
|
326
|
+
def _run():
|
|
327
|
+
try:
|
|
328
|
+
from calc_terminal import vision as _vis
|
|
329
|
+
ans, meta = _vis.analyze(self._attached_image, user_text=prompt)
|
|
330
|
+
return ans, meta, None
|
|
331
|
+
except Exception as e:
|
|
332
|
+
return None, None, str(e)
|
|
333
|
+
|
|
334
|
+
self._analyze_worker(_run, prompt, selection_only)
|
|
335
|
+
|
|
336
|
+
@staticmethod
|
|
337
|
+
def _fmt_meta(meta):
|
|
338
|
+
if not meta:
|
|
339
|
+
return ""
|
|
340
|
+
act = meta.get("action","")
|
|
341
|
+
wh = ""
|
|
342
|
+
if "width" in meta and "height" in meta:
|
|
343
|
+
wh = f"{meta['width']}x{meta['height']}"
|
|
344
|
+
return f"{act} {wh}".strip()
|
|
345
|
+
|
|
346
|
+
def _analyze_worker(self, runner, prompt, selection_only):
|
|
347
|
+
# wrapper to run in thread and post result back
|
|
348
|
+
import threading
|
|
349
|
+
def _thread():
|
|
350
|
+
ans, meta, err = runner()
|
|
351
|
+
# schedule UI update on main thread
|
|
352
|
+
try:
|
|
353
|
+
self.call_from_thread(self._on_analyze_done, ans, meta, err, selection_only)
|
|
354
|
+
except Exception:
|
|
355
|
+
pass
|
|
356
|
+
th = threading.Thread(target=_thread, daemon=True)
|
|
357
|
+
th.start()
|
|
358
|
+
|
|
359
|
+
def _on_analyze_done(self, ans, meta, err, selection_only):
|
|
360
|
+
# replace last "Analyzing..." bubble with real answer
|
|
361
|
+
# find last cat message and update it
|
|
362
|
+
try:
|
|
363
|
+
log = self.query_one("#vision-chat-log", VerticalScroll)
|
|
364
|
+
# last child is the "Analyzing..." placeholder
|
|
365
|
+
kids = list(log.children)
|
|
366
|
+
if kids:
|
|
367
|
+
last = kids[-1]
|
|
368
|
+
if err:
|
|
369
|
+
last.update(f"[b #f7768e]Error:[/] {err}\\nCheck model config via /model.")
|
|
370
|
+
else:
|
|
371
|
+
info = self._fmt_meta(meta)
|
|
372
|
+
suffix = f"\\n[dim]({info})[/]" if info else ""
|
|
373
|
+
# escape markup brackets in answer
|
|
374
|
+
safe_ans = (ans or "No answer.").replace("[", "\\[")
|
|
375
|
+
last.update(f"[b #9ece6a]CAT:[/] {safe_ans}{suffix}")
|
|
376
|
+
log.scroll_end(animate=False)
|
|
377
|
+
return
|
|
378
|
+
except Exception:
|
|
379
|
+
pass
|
|
380
|
+
# fallback push
|
|
381
|
+
if err:
|
|
382
|
+
self._push_chat("cat", f"Error: {err}")
|
|
383
|
+
else:
|
|
384
|
+
self._push_chat("cat", ans or "No answer.")
|
|
385
|
+
|
|
386
|
+
# ---- events ----
|
|
387
|
+
def on_button_pressed(self, event):
|
|
388
|
+
bid = event.button.id
|
|
389
|
+
# Extension disabled guard
|
|
390
|
+
if bid == "vision-open-ext":
|
|
391
|
+
try:
|
|
392
|
+
self.dismiss(None)
|
|
393
|
+
from .extensions_panel import ExtensionsPanel
|
|
394
|
+
self.app.push_screen(ExtensionsPanel())
|
|
395
|
+
except Exception:
|
|
396
|
+
self.dismiss(None)
|
|
397
|
+
return
|
|
398
|
+
if bid in ("vision-close2", "vision-close"):
|
|
399
|
+
self.dismiss(None)
|
|
400
|
+
return
|
|
401
|
+
if bid == "vision-start":
|
|
402
|
+
self._session_live = True
|
|
403
|
+
self._frame_count = max(self._frame_count, 1)
|
|
404
|
+
self._refresh_chrome()
|
|
405
|
+
self._set_preview("Session LIVE.\nAttach an image to analyze.\nUse annotation tools above to mark the region, then chat.")
|
|
406
|
+
self._push_chat("sys", "Session started. Attach an image to begin.")
|
|
407
|
+
elif bid == "vision-stop":
|
|
408
|
+
self._session_live = False
|
|
409
|
+
self._refresh_chrome()
|
|
410
|
+
self._set_preview("Session stopped.\nStart again to create a new capture.")
|
|
411
|
+
self._push_chat("sys", "Session stopped. Resources cleaned up.")
|
|
412
|
+
elif bid == "vision-pause":
|
|
413
|
+
self._push_chat("sys", "Paused.")
|
|
414
|
+
elif bid == "vision-resume":
|
|
415
|
+
self._push_chat("sys", "Resumed.")
|
|
416
|
+
elif bid == "vision-clear":
|
|
417
|
+
self._ann_count = 0
|
|
418
|
+
self._attached_image = None
|
|
419
|
+
self._frame_count = 0
|
|
420
|
+
self._refresh_chrome()
|
|
421
|
+
self._set_preview("Cleared. No image attached.")
|
|
422
|
+
self._push_chat("sys", "Cleared annotations and attached image.")
|
|
423
|
+
elif bid == "vision-attach":
|
|
424
|
+
path = self._pick_image_via_dialog()
|
|
425
|
+
if path:
|
|
426
|
+
self._attach_image(path)
|
|
427
|
+
else:
|
|
428
|
+
self._push_chat("sys", "No file chosen. Type an image path in the input and press Attach Image again.")
|
|
429
|
+
try:
|
|
430
|
+
inp = self.query_one("#vision-input", Input)
|
|
431
|
+
typed = inp.value.strip()
|
|
432
|
+
if typed and os.path.isfile(os.path.expanduser(typed)):
|
|
433
|
+
self._attach_image(os.path.expanduser(typed))
|
|
434
|
+
inp.value = ""
|
|
435
|
+
else:
|
|
436
|
+
if typed:
|
|
437
|
+
self._push_chat("sys", f"Not found: {typed}")
|
|
438
|
+
except Exception:
|
|
439
|
+
pass
|
|
440
|
+
elif bid in ("vision-pen","vision-highlight","vision-circle","vision-rect","vision-arrow","vision-text"):
|
|
441
|
+
tool = bid.replace("vision-","")
|
|
442
|
+
self._highlight_tool(tool)
|
|
443
|
+
name_map = {"pen":"Pen","highlight":"Highlight","circle":"Circle ○","rect":"Rect","arrow":"Arrow","text":"Text"}
|
|
444
|
+
self._set_preview(f"Tool: {name_map.get(tool, tool)}\nMark the region on the attached image, then describe it in chat.")
|
|
445
|
+
self._ann_count += 1
|
|
446
|
+
self._refresh_chrome()
|
|
447
|
+
self._push_chat("sys", f"Tool selected: {name_map.get(tool, tool)} (annotations stay in this CAT Vision session)")
|
|
448
|
+
elif bid in ("vision-undo","vision-redo"):
|
|
449
|
+
self._push_chat("sys", "Undo/Redo for the annotation list in this session.")
|
|
450
|
+
elif bid == "vision-analyze-sel":
|
|
451
|
+
self._do_analyze(selection_only=True)
|
|
452
|
+
elif bid == "vision-analyze-all":
|
|
453
|
+
self._do_analyze(selection_only=False)
|
|
454
|
+
elif bid == "vision-fix":
|
|
455
|
+
self._push_chat("cat", "I can propose a fix for the selected region. Describe the expected behavior and I will generate a patch.\nFile edits require your permission (Ask / Full) - nothing is changed without approval.")
|
|
456
|
+
elif bid == "vision-send":
|
|
457
|
+
try:
|
|
458
|
+
inp = self.query_one("#vision-input", Input)
|
|
459
|
+
txt = inp.value.strip()
|
|
460
|
+
if not txt:
|
|
461
|
+
return
|
|
462
|
+
if not self._attached_image:
|
|
463
|
+
self._push_chat("you", txt)
|
|
464
|
+
inp.value = ""
|
|
465
|
+
self._push_chat("cat", "Attach an image to enable visual analysis, or keep chatting - I will use project context and your description.")
|
|
466
|
+
return
|
|
467
|
+
inp.value = txt
|
|
468
|
+
self._do_analyze(selection_only=True)
|
|
469
|
+
except Exception:
|
|
470
|
+
pass
|
|
471
|
+
|
|
472
|
+
def on_input_submitted(self, event):
|
|
473
|
+
if event.input.id == "vision-input":
|
|
474
|
+
# Enter in the input should Send (same as button)
|
|
475
|
+
self.on_button_pressed(type("E", (), {"button": type("B", (), {"id": "vision-send"})()})())
|
|
476
|
+
|
|
477
|
+
def action_cancel(self):
|
|
478
|
+
# Esc: if there is live session, stop it cleanly before dismiss?
|
|
479
|
+
self.dismiss(None)
|
|
480
|
+
|
|
481
|
+
def action_send(self):
|
|
482
|
+
self.on_button_pressed(type("E", (), {"button": type("B", (), {"id": "vision-send"})()})())
|
|
483
|
+
|
|
484
|
+
def on_key(self, event):
|
|
485
|
+
if event.key == "escape":
|
|
486
|
+
self.dismiss(None)
|
|
487
|
+
|
|
488
|
+
else:
|
|
489
|
+
VisionPanel = None
|