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
calc_terminal/vision.py
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT v0.7.9.0 — vision.py: the real image / multimodal pipeline.
|
|
3
|
+
|
|
4
|
+
image
|
|
5
|
+
↓ validate (exists, decodable, size caps)
|
|
6
|
+
↓ decode/normalize (EXIF orientation, color mode, alpha, format)
|
|
7
|
+
↓ classify (photo / screenshot / scanned document / chart /
|
|
8
|
+
│ diagram / code screenshot / handwritten)
|
|
9
|
+
↓ strategy (resize/compress only as needed — quality kept)
|
|
10
|
+
↓ vision-capable model (chosen by model_router when the active one
|
|
11
|
+
│ can't see — never just "model can't see this")
|
|
12
|
+
↓ structured analysis (comprehensive extraction prompt per kind)
|
|
13
|
+
|
|
14
|
+
Rules:
|
|
15
|
+
* The image BYTES actually travel to the model (base64 data URL), never
|
|
16
|
+
just the filename.
|
|
17
|
+
* Normalization never degrades quality unnecessarily: images within the
|
|
18
|
+
budget pass through byte-for-byte; oversized ones are downscaled with
|
|
19
|
+
a high-quality filter and/or recompressed at high quality.
|
|
20
|
+
* Corrupted/undecodable files produce an explicit honest error — never a
|
|
21
|
+
fabricated description.
|
|
22
|
+
* Pillow is used when available (TIFF/BMP/WEBP conversion, EXIF fix);
|
|
23
|
+
without it, provider-native formats still work and TIFF/BMP get an
|
|
24
|
+
honest "cannot normalize" note instead of silent breakage.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
print("This is a library file and is not meant to be run directly.")
|
|
29
|
+
import sys
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
|
|
32
|
+
import base64
|
|
33
|
+
import io
|
|
34
|
+
import os
|
|
35
|
+
from typing import Dict, Optional, Tuple
|
|
36
|
+
|
|
37
|
+
# Provider-safe formats + generous-but-sane transport budget.
|
|
38
|
+
MAX_DIMENSION = 1568 # long-edge cap (matches common vision models' tile grid)
|
|
39
|
+
MAX_BYTES = 4 * 1024 * 1024 # ~4 MB payload after normalization (pre-base64)
|
|
40
|
+
JPEG_QUALITY = 88
|
|
41
|
+
|
|
42
|
+
PROVIDER_NATIVE = {".png", ".jpg", ".jpeg", ".gif", ".webp"}
|
|
43
|
+
ALL_IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp",
|
|
44
|
+
".tiff", ".tif", ".ico"}
|
|
45
|
+
|
|
46
|
+
_MIME = {".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png",
|
|
47
|
+
".gif": "image/gif", ".webp": "image/webp", ".bmp": "image/bmp",
|
|
48
|
+
".tiff": "image/tiff", ".tif": "image/tiff", ".ico": "image/x-icon"}
|
|
49
|
+
|
|
50
|
+
_PIL = None
|
|
51
|
+
_PIL_TRIED = False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _pil():
|
|
55
|
+
global _PIL, _PIL_TRIED
|
|
56
|
+
if not _PIL_TRIED:
|
|
57
|
+
_PIL_TRIED = True
|
|
58
|
+
try:
|
|
59
|
+
from PIL import Image # noqa: N811
|
|
60
|
+
_PIL = Image
|
|
61
|
+
except Exception:
|
|
62
|
+
_PIL = None
|
|
63
|
+
return _PIL
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def pil_available() -> bool:
|
|
67
|
+
return _pil() is not None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def ocr_available() -> bool:
|
|
71
|
+
"""True when a real OCR engine (pytesseract + tesseract binary) exists.
|
|
72
|
+
Never assumed — checked, honestly reported."""
|
|
73
|
+
try:
|
|
74
|
+
import pytesseract # noqa: F401
|
|
75
|
+
import shutil
|
|
76
|
+
return bool(shutil.which("tesseract"))
|
|
77
|
+
except Exception:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ImageError(Exception):
|
|
82
|
+
"""Raised for genuinely unreadable/corrupted/missing images."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ------------------------------------------------------------- validation --
|
|
86
|
+
|
|
87
|
+
def validate(path: str) -> int:
|
|
88
|
+
"""VALIDATE stage: existence, type, non-empty, sane size.
|
|
89
|
+
Returns file size in bytes; raises ImageError with the real reason."""
|
|
90
|
+
if not path or not os.path.lexists(path):
|
|
91
|
+
raise ImageError(f"image not found: {path}")
|
|
92
|
+
if os.path.isdir(path):
|
|
93
|
+
raise ImageError(f"path is a directory, not an image: {path}")
|
|
94
|
+
ext = os.path.splitext(path)[1].lower()
|
|
95
|
+
if ext not in ALL_IMAGE_EXTS:
|
|
96
|
+
raise ImageError(f"unsupported image extension: {ext or '(none)'}")
|
|
97
|
+
try:
|
|
98
|
+
size = os.path.getsize(path)
|
|
99
|
+
except OSError as e:
|
|
100
|
+
raise ImageError(f"cannot stat image: {e}")
|
|
101
|
+
if size == 0:
|
|
102
|
+
raise ImageError("image file is empty (0 bytes)")
|
|
103
|
+
if size > 64 * 1024 * 1024:
|
|
104
|
+
raise ImageError("image larger than the 64 MB safety limit")
|
|
105
|
+
# Magic-byte sniff: catch renamed/corrupted files before decode time.
|
|
106
|
+
try:
|
|
107
|
+
with open(path, "rb") as f:
|
|
108
|
+
head = f.read(16)
|
|
109
|
+
except OSError as e:
|
|
110
|
+
raise ImageError(f"cannot read image bytes: {e}")
|
|
111
|
+
if not head:
|
|
112
|
+
raise ImageError("image file is unreadable")
|
|
113
|
+
return size
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------------ classification --
|
|
117
|
+
|
|
118
|
+
_CODE_IMG_HINTS = ("code", "screenshot_snippet", "ide", "editor")
|
|
119
|
+
_DOC_HINTS = ("document", "scan", "receipt", "invoice", "page", "paper", "letter")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def classify_image(path: str, prompt: str = "") -> str:
|
|
123
|
+
"""SMART PIPELINE stage: best-effort local classifier for choosing the
|
|
124
|
+
processing strategy. Returns one of:
|
|
125
|
+
photo | screenshot | scanned_document | chart | diagram |
|
|
126
|
+
code_screenshot | handwritten.
|
|
127
|
+
|
|
128
|
+
Honest heuristic: filename hints + user wording + aspect ratio +
|
|
129
|
+
format priors. It biases the analysis PROMPT, it does not fabricate
|
|
130
|
+
facts about the content."""
|
|
131
|
+
name = os.path.basename(path).lower()
|
|
132
|
+
text = ((prompt or "") + " " + name).lower()
|
|
133
|
+
|
|
134
|
+
if any(h in text for h in ("handwrit", "hand-written", "notes photo")):
|
|
135
|
+
return "handwritten"
|
|
136
|
+
if any(h in text for h in _CODE_IMG_HINTS) or any(
|
|
137
|
+
h in name for h in ("code", "snippet", "ide_", "vscode", "screenshot_code")):
|
|
138
|
+
return "code_screenshot"
|
|
139
|
+
if any(h in text for h in ("chart", "graph", "plot", "bar ", "pie ")) \
|
|
140
|
+
or "chart" in name or "plot" in name:
|
|
141
|
+
return "chart"
|
|
142
|
+
if any(h in text for h in ("diagram", "flowchart", "uml", "architecture diagram",
|
|
143
|
+
"sequence diagram", "er diagram")) or "diagram" in name:
|
|
144
|
+
return "diagram"
|
|
145
|
+
if any(h in text for h in _DOC_HINTS) or any(h in name for h in ("scan", "doc", "receipt", "invoice")):
|
|
146
|
+
return "scanned_document"
|
|
147
|
+
if "screenshot" in text or "screen shot" in text or "screen-" in name or "screenshot" in name:
|
|
148
|
+
return "screenshot"
|
|
149
|
+
|
|
150
|
+
# Aspect/format priors: tall multi-page scans vs typical photos.
|
|
151
|
+
try:
|
|
152
|
+
info = inspect(path)
|
|
153
|
+
w, h = info.get("width", 0), info.get("height", 0)
|
|
154
|
+
fmt = str(info.get("format", "")).upper()
|
|
155
|
+
if w and h:
|
|
156
|
+
ratio = max(w, h) / max(1, min(w, h))
|
|
157
|
+
if ratio > 2.8 and min(w, h) > 800:
|
|
158
|
+
return "scanned_document"
|
|
159
|
+
if fmt == "PNG" and ratio < 1.6 and max(w, h) <= 2400:
|
|
160
|
+
return "screenshot"
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
return "photo"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ----------------------------------------------------------------- inspect --
|
|
167
|
+
|
|
168
|
+
def inspect(path: str) -> Dict:
|
|
169
|
+
"""Real metadata about an image (dimensions, format, mode) — no
|
|
170
|
+
fabrication; empty dict fields when Pillow isn't available."""
|
|
171
|
+
out: Dict = {"width": 0, "height": 0, "format": "", "mode": ""}
|
|
172
|
+
Image = _pil()
|
|
173
|
+
if Image is None:
|
|
174
|
+
return out
|
|
175
|
+
try:
|
|
176
|
+
with Image.open(path) as im:
|
|
177
|
+
out["width"], out["height"] = im.size
|
|
178
|
+
out["format"] = (im.format or "").upper()
|
|
179
|
+
out["mode"] = (im.mode or "").upper()
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
return out
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ------------------------------------------------------------- normalize --
|
|
186
|
+
|
|
187
|
+
def _exif_transpose(im):
|
|
188
|
+
"""Apply EXIF orientation so photos taken sideways/right-way-down
|
|
189
|
+
display correctly. No-op for images without EXIF orientation."""
|
|
190
|
+
try:
|
|
191
|
+
from PIL import ImageOps
|
|
192
|
+
return ImageOps.exif_transpose(im)
|
|
193
|
+
except Exception:
|
|
194
|
+
return im
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _needs_conversion(fmt: str, mode: str, ext: str) -> bool:
|
|
198
|
+
"""Formats/modes providers reject or mishandle."""
|
|
199
|
+
if ext == ".tiff" or ext == ".tif":
|
|
200
|
+
return True # TIFF rarely accepted by vision APIs
|
|
201
|
+
if ext == ".bmp":
|
|
202
|
+
return True # BMP is huge; convert to PNG/JPEG
|
|
203
|
+
if fmt in ("TIFF", "BMP", "MPO", "PPM"):
|
|
204
|
+
return True
|
|
205
|
+
if mode in ("CMYK", "I;16", "1"):
|
|
206
|
+
return True
|
|
207
|
+
if fmt == "GIF" and ext == ".gif":
|
|
208
|
+
return False # static GIFs are accepted natively
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def normalize(path: str,
|
|
213
|
+
max_dimension: int = MAX_DIMENSION,
|
|
214
|
+
max_bytes: int = MAX_BYTES) -> Tuple[str, bytes, Dict]:
|
|
215
|
+
"""NORMALIZE stage: validate → decode → EXIF-fix → convert/rescale/
|
|
216
|
+
recompress ONLY as needed. Returns (mime, raw_bytes, meta).
|
|
217
|
+
|
|
218
|
+
meta includes what was actually done (honest reporting):
|
|
219
|
+
action: passthrough | converted | resized | resized+converted | error
|
|
220
|
+
Raises ImageError for genuinely corrupted files."""
|
|
221
|
+
size = validate(path)
|
|
222
|
+
ext = os.path.splitext(path)[1].lower()
|
|
223
|
+
meta = {"source_size": size, "action": "passthrough",
|
|
224
|
+
"normalized": False, "pil": pil_available()}
|
|
225
|
+
|
|
226
|
+
Image = _pil()
|
|
227
|
+
if Image is None:
|
|
228
|
+
# No Pillow: native formats pass through untouched; exotic ones
|
|
229
|
+
# fail loudly and honestly here rather than at the API.
|
|
230
|
+
if ext in PROVIDER_NATIVE:
|
|
231
|
+
with open(path, "rb") as f:
|
|
232
|
+
return _MIME[ext], f.read(), meta
|
|
233
|
+
raise ImageError(
|
|
234
|
+
f"{ext} images need Pillow ('pip install pillow') to be "
|
|
235
|
+
f"converted into a provider-compatible format")
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
with Image.open(path) as im:
|
|
239
|
+
im.load()
|
|
240
|
+
fmt = (im.format or "").upper()
|
|
241
|
+
mode = im.mode or ""
|
|
242
|
+
meta.update({"width": im.size[0], "height": im.size[1],
|
|
243
|
+
"format": fmt, "mode": mode})
|
|
244
|
+
needs_convert = _needs_conversion(fmt, mode, ext)
|
|
245
|
+
too_big_dim = max(im.size) > max_dimension
|
|
246
|
+
|
|
247
|
+
if not needs_convert and not too_big_dim and size <= max_bytes:
|
|
248
|
+
# Quality rule: nothing to do → original bytes verbatim.
|
|
249
|
+
with open(path, "rb") as f:
|
|
250
|
+
meta["action"] = "passthrough"
|
|
251
|
+
return _MIME.get(ext, "image/png"), f.read(), meta
|
|
252
|
+
|
|
253
|
+
work = _exif_transpose(im)
|
|
254
|
+
if work is not im:
|
|
255
|
+
meta["exif_rotated"] = True
|
|
256
|
+
needs_convert = needs_convert or ext not in PROVIDER_NATIVE \
|
|
257
|
+
or True # re-encoded after transpose; pick encoder below
|
|
258
|
+
|
|
259
|
+
if too_big_dim:
|
|
260
|
+
work.thumbnail((max_dimension, max_dimension),
|
|
261
|
+
getattr(Image, "LANCZOS", 1))
|
|
262
|
+
meta["resized_to"] = list(work.size)
|
|
263
|
+
|
|
264
|
+
has_alpha = "A" in work.getbands() or work.mode == "P"
|
|
265
|
+
if has_alpha and not needs_convert:
|
|
266
|
+
target_fmt, target_mime = "PNG", "image/png"
|
|
267
|
+
elif has_alpha:
|
|
268
|
+
target_fmt, target_mime = "PNG", "image/png"
|
|
269
|
+
else:
|
|
270
|
+
target_fmt, target_mime = "JPEG", "image/jpeg"
|
|
271
|
+
if work.mode != "RGB":
|
|
272
|
+
work = work.convert("RGB")
|
|
273
|
+
|
|
274
|
+
buf = io.BytesIO()
|
|
275
|
+
save_kwargs = {}
|
|
276
|
+
if target_fmt == "JPEG":
|
|
277
|
+
save_kwargs = {"quality": JPEG_QUALITY, "optimize": True}
|
|
278
|
+
elif target_fmt == "PNG":
|
|
279
|
+
save_kwargs = {"optimize": True}
|
|
280
|
+
work.save(buf, format=target_fmt, **save_kwargs)
|
|
281
|
+
data = buf.getvalue()
|
|
282
|
+
|
|
283
|
+
# One extra recompression pass if still over budget (rare).
|
|
284
|
+
attempts = 0
|
|
285
|
+
while len(data) > max_bytes and target_fmt == "JPEG" and attempts < 3:
|
|
286
|
+
q = max(50, JPEG_QUALITY - 15 * (attempts + 1))
|
|
287
|
+
buf = io.BytesIO()
|
|
288
|
+
work.save(buf, format="JPEG", quality=q, optimize=True)
|
|
289
|
+
data = buf.getvalue()
|
|
290
|
+
meta["recompressed_quality"] = q
|
|
291
|
+
attempts += 1
|
|
292
|
+
if len(data) > max_bytes:
|
|
293
|
+
raise ImageError(
|
|
294
|
+
"image cannot be normalized under the transport budget "
|
|
295
|
+
"without unacceptable quality loss")
|
|
296
|
+
|
|
297
|
+
meta["action"] = ("resized+converted" if too_big_dim else "converted") \
|
|
298
|
+
if needs_convert or too_big_dim else "recompressed"
|
|
299
|
+
meta["normalized"] = meta["action"] != "passthrough"
|
|
300
|
+
meta["out_format"] = target_fmt
|
|
301
|
+
return target_mime, data, meta
|
|
302
|
+
except ImageError:
|
|
303
|
+
raise
|
|
304
|
+
except Exception as e:
|
|
305
|
+
raise ImageError(f"corrupted or undecodable image ({type(e).__name__}): {e}")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def encode_for_model(path: str, prompt: str = "",
|
|
309
|
+
max_dimension: int = MAX_DIMENSION,
|
|
310
|
+
max_bytes: int = MAX_BYTES) -> Tuple[str, str, Dict]:
|
|
311
|
+
"""The ONE call the attachment/aicore layer uses:
|
|
312
|
+
returns (mime, base64_payload, meta) for a normalized image."""
|
|
313
|
+
import time as _time
|
|
314
|
+
t0 = _time.perf_counter()
|
|
315
|
+
mime, data, meta = normalize(path, max_dimension=max_dimension,
|
|
316
|
+
max_bytes=max_bytes)
|
|
317
|
+
b64 = base64.b64encode(data).decode("ascii")
|
|
318
|
+
try:
|
|
319
|
+
from . import metrics
|
|
320
|
+
m = metrics.current()
|
|
321
|
+
if m is not None:
|
|
322
|
+
m.mark_stage("vision", t0)
|
|
323
|
+
except Exception:
|
|
324
|
+
pass
|
|
325
|
+
meta["payload_b64_chars"] = len(b64)
|
|
326
|
+
return mime, b64, meta
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# ------------------------------------------------------------- strategies --
|
|
330
|
+
|
|
331
|
+
_KIND_PROMPTS = {
|
|
332
|
+
"photo": (
|
|
333
|
+
"Describe this photograph thoroughly: main subjects, setting, colors, "
|
|
334
|
+
"lighting, notable details, visible text or brands, and anything unusual."),
|
|
335
|
+
"screenshot": (
|
|
336
|
+
"This is a screen capture. Transcribe all visible UI elements, window "
|
|
337
|
+
"titles, menus, buttons, error messages, code (verbatim, in fenced "
|
|
338
|
+
"blocks), URLs, and values. Explain what application/screen this is."),
|
|
339
|
+
"scanned_document": (
|
|
340
|
+
"This is a scanned/document image. Extract ALL text verbatim preserving "
|
|
341
|
+
"structure (headings, paragraphs, lists, tables as markdown tables). "
|
|
342
|
+
"Include page metadata if visible (title, dates, signatures, stamps)."),
|
|
343
|
+
"chart": (
|
|
344
|
+
"Analyze this chart/graph: state the chart type, axes and their units, "
|
|
345
|
+
"every series/legend entry, key data points (read approximate values), "
|
|
346
|
+
"trends, anomalies, and the takeaway conclusion."),
|
|
347
|
+
"diagram": (
|
|
348
|
+
"Analyze this diagram: identify the diagram type, every node/component "
|
|
349
|
+
"(with labels), every connection/arrow and its direction, groupings, "
|
|
350
|
+
"and reconstruct its structure as an indented outline or mermaid-style "
|
|
351
|
+
"text description."),
|
|
352
|
+
"code_screenshot": (
|
|
353
|
+
"This screenshot contains source code. Transcribe the code VERBATIM in "
|
|
354
|
+
"a fenced block with the correct language tag, then explain what it "
|
|
355
|
+
"does, note any bugs/issues you can see, and mention visible file "
|
|
356
|
+
"names, line numbers or IDE context."),
|
|
357
|
+
"handwritten": (
|
|
358
|
+
"This appears to contain handwriting. Transcribe ALL handwritten "
|
|
359
|
+
"content as faithfully as possible, marking unclear words with [?]. "
|
|
360
|
+
"Preserve layout, numbering, equations (as LaTeX-ish plain text) and "
|
|
361
|
+
"any printed content separately."),
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
BASE_ANALYSIS_PROMPT = (
|
|
365
|
+
"Analyze the attached image comprehensively. Wherever supported by the "
|
|
366
|
+
"image content, extract: visible objects and scene; ALL text (OCR); "
|
|
367
|
+
"tables (as markdown); charts (axes, series, values, trends); diagrams "
|
|
368
|
+
"(nodes, edges, structure); UI elements; code shown (verbatim in fenced "
|
|
369
|
+
"blocks); document layout; mathematical expressions; visual relationships; "
|
|
370
|
+
"and notable metadata. Be specific and complete — do NOT merely say what "
|
|
371
|
+
"kind of thing the image is."
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def analysis_prompt(kind: str, user_text: str = "") -> str:
|
|
376
|
+
"""Strategy-specific instruction combined with the user's actual ask.
|
|
377
|
+
Used by both the direct-vision path and the agent's analyze_image tool."""
|
|
378
|
+
specific = _KIND_PROMPTS.get(kind, BASE_ANALYSIS_PROMPT)
|
|
379
|
+
ask = (user_text or "").strip()
|
|
380
|
+
ocr_note = ""
|
|
381
|
+
pre_ocr = ""
|
|
382
|
+
if kind in ("scanned_document", "handwritten", "screenshot",
|
|
383
|
+
"code_screenshot", "chart", "diagram"):
|
|
384
|
+
if ocr_available():
|
|
385
|
+
ocr_note = ("\n\nA local OCR engine is available; combine its raw "
|
|
386
|
+
"output (provided below) with your own visual reading "
|
|
387
|
+
"— reconcile differences explicitly.")
|
|
388
|
+
else:
|
|
389
|
+
ocr_note = ("\n\nPerform careful character-level OCR yourself as "
|
|
390
|
+
"part of the analysis.")
|
|
391
|
+
prefix = f"The user asked: \"{ask}\"\n\n" if ask and ask.lower() not in (
|
|
392
|
+
"analyze this image", "describe this image") else ""
|
|
393
|
+
return (prefix + BASE_ANALYSIS_PROMPT + "\n\nImage-type strategy ("
|
|
394
|
+
+ kind + "): " + specific + ocr_note + pre_ocr)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def run_local_ocr(path: str) -> str:
|
|
398
|
+
"""Best-effort local OCR via pytesseract when installed. Returns ''
|
|
399
|
+
(not an error) when unavailable — callers fold it into the prompt."""
|
|
400
|
+
if not ocr_available():
|
|
401
|
+
return ""
|
|
402
|
+
try:
|
|
403
|
+
import pytesseract
|
|
404
|
+
from PIL import Image
|
|
405
|
+
with Image.open(path) as im:
|
|
406
|
+
return pytesseract.image_to_string(im) or ""
|
|
407
|
+
except Exception:
|
|
408
|
+
return ""
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# ------------------------------------------------------------ model bridge --
|
|
412
|
+
|
|
413
|
+
def find_vision_config():
|
|
414
|
+
"""Bridge to the router: a config dict for a vision-capable configured
|
|
415
|
+
model, or None when none exists (callers report that honestly)."""
|
|
416
|
+
try:
|
|
417
|
+
from . import model_router
|
|
418
|
+
cfg, caps = model_router.find_vision_capable()
|
|
419
|
+
return cfg
|
|
420
|
+
except Exception:
|
|
421
|
+
return None
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def analyze(path: str, user_text: str = "", history=None,
|
|
425
|
+
config: Optional[dict] = None):
|
|
426
|
+
"""Full VISION PATH: normalize → classify (+local OCR when available)
|
|
427
|
+
→ vision-capable model → structured answer string. Uses the given
|
|
428
|
+
config when it can see, otherwise reroutes to a capable backup;
|
|
429
|
+
raises ImageError for bad files and returns an honest message when no
|
|
430
|
+
vision-capable model is configured at all."""
|
|
431
|
+
mime, b64, meta = encode_for_model(path, user_text)
|
|
432
|
+
kind = classify_image(path, user_text)
|
|
433
|
+
prompt = analysis_prompt(kind, user_text)
|
|
434
|
+
local_text = run_local_ocr(path)
|
|
435
|
+
if local_text.strip():
|
|
436
|
+
prompt += "\n\nLOCAL OCR OUTPUT (machine-read, may contain errors):\n" \
|
|
437
|
+
+ local_text[:4000]
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
from .event_stream import stream, IMAGE_ANALYSIS_STARTED, IMAGE_ANALYSIS_FINISHED
|
|
441
|
+
stream.emit(IMAGE_ANALYSIS_STARTED, source="vision", path=os.path.basename(path),
|
|
442
|
+
kind=kind, action=meta.get("action"))
|
|
443
|
+
except Exception:
|
|
444
|
+
pass
|
|
445
|
+
|
|
446
|
+
cfg = config
|
|
447
|
+
if cfg is not None:
|
|
448
|
+
try:
|
|
449
|
+
from .attachments import provider_capabilities
|
|
450
|
+
if not provider_capabilities(cfg).get("vision"):
|
|
451
|
+
cfg = None
|
|
452
|
+
except Exception:
|
|
453
|
+
cfg = None
|
|
454
|
+
if cfg is None:
|
|
455
|
+
cfg = find_vision_config()
|
|
456
|
+
if cfg is None:
|
|
457
|
+
return ("No vision-capable AI model is currently configured, so I "
|
|
458
|
+
"cannot visually analyze this image. Configure one (e.g. a "
|
|
459
|
+
"gpt-4o / claude / gemini-class vision model via /model) and "
|
|
460
|
+
"attach it again.", meta)
|
|
461
|
+
|
|
462
|
+
answer = _query_vision_model(cfg, prompt, mime, b64, system_extra=kind)
|
|
463
|
+
try:
|
|
464
|
+
from .event_stream import stream, IMAGE_ANALYSIS_FINISHED
|
|
465
|
+
stream.emit(IMAGE_ANALYSIS_FINISHED, source="vision", path=os.path.basename(path),
|
|
466
|
+
kind=kind, chars=len(answer or ""))
|
|
467
|
+
except Exception:
|
|
468
|
+
pass
|
|
469
|
+
return answer, meta
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _query_vision_model(config: dict, prompt: str, mime: str, b64: str,
|
|
473
|
+
system_extra: str = "") -> str:
|
|
474
|
+
"""One vision completion against an explicit provider config, using
|
|
475
|
+
aicore's own request machinery through the public attachments path
|
|
476
|
+
(keeps per-provider payload shaping in exactly one place)."""
|
|
477
|
+
try:
|
|
478
|
+
from . import aicore
|
|
479
|
+
try:
|
|
480
|
+
from . import identity
|
|
481
|
+
system_prompt = ("You are CAT Vision, CAT's precise image-analysis "
|
|
482
|
+
"specialist. Be exhaustive and concrete.\n\n"
|
|
483
|
+
+ identity.IDENTITY_BLOCK)
|
|
484
|
+
except Exception:
|
|
485
|
+
system_prompt = ("You are CAT Vision, CAT's precise image-analysis "
|
|
486
|
+
"specialist. Be exhaustive and concrete.")
|
|
487
|
+
att = _temp_attachment(mime, b64)
|
|
488
|
+
return aicore.query_ai(prompt, system_prompt=system_prompt,
|
|
489
|
+
history=None, config=config,
|
|
490
|
+
attachments=[att], size_class="large")
|
|
491
|
+
except Exception as e:
|
|
492
|
+
return f"Image analysis failed: {e}"
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _temp_attachment(mime, b64):
|
|
496
|
+
"""Build a minimal Attachment-like object carrying an already-encoded
|
|
497
|
+
image payload so aicore._user_content can shape it per-provider
|
|
498
|
+
without re-reading or re-normalizing the file."""
|
|
499
|
+
from .attachments import Attachment
|
|
500
|
+
att = Attachment.__new__(Attachment)
|
|
501
|
+
att.id = f"att-vision-{abs(hash(b64[:64])) % 1000000}"
|
|
502
|
+
att.name = "image"
|
|
503
|
+
att.path = ""
|
|
504
|
+
att.extension = "." + mime.split("/")[-1].split("+")[0]
|
|
505
|
+
att.mime_type = mime
|
|
506
|
+
att.size = 0
|
|
507
|
+
att.kind = "image"
|
|
508
|
+
att.content = "[native image payload]"
|
|
509
|
+
att.metadata = {"inline_b64": b64}
|
|
510
|
+
att.extraction_status = "ready"
|
|
511
|
+
att.error = None
|
|
512
|
+
att.created_at = 0.0
|
|
513
|
+
att.source = "pipeline"
|
|
514
|
+
return att
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT VS Code integration (spec 28).
|
|
3
|
+
|
|
4
|
+
Exposes CAT Vision commands to VS Code panel. In pure Python (no VS Code),
|
|
5
|
+
these are graceful stubs that still report honest status and allow the CLI
|
|
6
|
+
and TUI to route Vision context through the same code path:
|
|
7
|
+
|
|
8
|
+
CAT: Start Vision Session
|
|
9
|
+
CAT: Analyze Current Screen
|
|
10
|
+
CAT: Analyze Selected Region
|
|
11
|
+
CAT: Send Screenshot to CAT
|
|
12
|
+
CAT: Open CAT Vision
|
|
13
|
+
CAT: Stop Vision Session
|
|
14
|
+
|
|
15
|
+
When VS Code is the current project, Vision context may include:
|
|
16
|
+
VS Code active file + selected code + workspace + terminal
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
from typing import Dict, Optional, Tuple
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_CLI_CANDIDATES = ("code", "code-insiders", "codium", "code.cmd")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _find_cli() -> Optional[str]:
|
|
31
|
+
for name in _CLI_CANDIDATES:
|
|
32
|
+
p = shutil.which(name)
|
|
33
|
+
if p:
|
|
34
|
+
return p
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def status() -> Dict[str, object]:
|
|
39
|
+
cli = _find_cli()
|
|
40
|
+
return {
|
|
41
|
+
"available": cli is not None,
|
|
42
|
+
"cli_path": cli or "",
|
|
43
|
+
"vision_commands": [
|
|
44
|
+
"CAT: Start Vision Session",
|
|
45
|
+
"CAT: Analyze Current Screen",
|
|
46
|
+
"CAT: Analyze Selected Region",
|
|
47
|
+
"CAT: Send Screenshot to CAT",
|
|
48
|
+
"CAT: Open CAT Vision",
|
|
49
|
+
"CAT: Stop Vision Session",
|
|
50
|
+
],
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _run_code(args, cwd: Optional[str] = None) -> Tuple[bool, str]:
|
|
55
|
+
cli = _find_cli()
|
|
56
|
+
if not cli:
|
|
57
|
+
return False, "VS Code CLI not found (install VS Code and ensure 'code' is on PATH)."
|
|
58
|
+
try:
|
|
59
|
+
proc = subprocess.run(
|
|
60
|
+
[cli] + args,
|
|
61
|
+
cwd=cwd or os.getcwd(),
|
|
62
|
+
capture_output=True,
|
|
63
|
+
text=True,
|
|
64
|
+
timeout=8,
|
|
65
|
+
)
|
|
66
|
+
ok = proc.returncode == 0
|
|
67
|
+
msg = (proc.stdout or proc.stderr or "").strip() or ("OK" if ok else "failed")
|
|
68
|
+
return ok, msg[:400]
|
|
69
|
+
except Exception as e:
|
|
70
|
+
return False, str(e)[:400]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def open_workspace(path: str = "") -> Tuple[bool, str]:
|
|
74
|
+
path = (path or "").strip() or os.getcwd()
|
|
75
|
+
return _run_code([path])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def open_workspace_from_current() -> Tuple[bool, str]:
|
|
79
|
+
try:
|
|
80
|
+
from . import workspace as ws
|
|
81
|
+
root = ws.root_dir() or ws.active_project() or os.getcwd()
|
|
82
|
+
except Exception:
|
|
83
|
+
root = os.getcwd()
|
|
84
|
+
return _run_code([root])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def open_file(path: str, line: Optional[str] = None) -> Tuple[bool, str]:
|
|
88
|
+
path = (path or "").strip()
|
|
89
|
+
if not path:
|
|
90
|
+
return False, "No file path given."
|
|
91
|
+
arg = f"{path}:{line}" if line and str(line).isdigit() else path
|
|
92
|
+
return _run_code(["-g", arg])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def open_diff(a: str, b: str) -> Tuple[bool, str]:
|
|
96
|
+
return _run_code(["--diff", a, b])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Vision-specific helpers — Conceptually these route through the CAT Vision service.
|
|
100
|
+
# In the current runtime they acknowledge the request honestly and point to the panel.
|
|
101
|
+
def vision_command(name: str, file_path: str = "", selected_code: str = "") -> Tuple[bool, str]:
|
|
102
|
+
known = {
|
|
103
|
+
"CAT: Start Vision Session": "Vision session started (use the CAT Vision panel).",
|
|
104
|
+
"CAT: Analyze Current Screen": "Analyzing current screen via CAT Vision.",
|
|
105
|
+
"CAT: Analyze Selected Region": "Analyzing selected region via CAT Vision.",
|
|
106
|
+
"CAT: Send Screenshot to CAT": "Screenshot sent to CAT Vision (attach via /import or PWA Upload Screenshot).",
|
|
107
|
+
"CAT: Open CAT Vision": "Opening CAT Vision panel.",
|
|
108
|
+
"CAT: Stop Vision Session": "Vision session stopped.",
|
|
109
|
+
}
|
|
110
|
+
msg = known.get(name, f"Vision command '{name}' — context: file={file_path[:80]} selected={len(selected_code)} chars")
|
|
111
|
+
# In a real VS Code extension this would post to the CAT Vision service with workspace context.
|
|
112
|
+
# Here we keep the shared state promise: VisionSession lives in CAT Core, not per-UI.
|
|
113
|
+
return True, msg
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""CAT web package (Fatty CAT server + web runtime).
|
|
2
|
+
|
|
3
|
+
This package ships the FastAPI server and the bundled PWA static assets
|
|
4
|
+
(``static/``). The directory previously had no ``__init__.py`` and only
|
|
5
|
+
worked by accident via namespace-package semantics; an explicit package
|
|
6
|
+
marker makes ``calc_terminal.web`` importable identically from a source
|
|
7
|
+
checkout and from an installed wheel.
|
|
8
|
+
"""
|