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,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT v0.8.a — Screen Capture Engine.
|
|
3
|
+
|
|
4
|
+
Manages getDisplayMedia() / getMedia() capture via the frontend.
|
|
5
|
+
The server side handles config, state, and fallback logic.
|
|
6
|
+
Actual pixel capture happens in the browser; the server receives
|
|
7
|
+
base64-encoded frames via WebSocket or HTTP POST.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CaptureMode(str, Enum):
|
|
20
|
+
DISPLAY = "display" # getDisplayMedia — full screen / window / tab
|
|
21
|
+
MEDIA = "media" # getMedia with displaySurface hints
|
|
22
|
+
AUDIO_VIDEO = "audio_video"
|
|
23
|
+
AUDIO_ONLY = "audio_only"
|
|
24
|
+
TAB = "tab" # Browser tab capture
|
|
25
|
+
WINDOW = "window" # Specific window capture
|
|
26
|
+
NONE = "none" # No capture available
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class CaptureConfig:
|
|
31
|
+
mode: CaptureMode = CaptureMode.DISPLAY
|
|
32
|
+
fps: float = 1.0
|
|
33
|
+
quality: int = 85
|
|
34
|
+
max_frames: int = 500
|
|
35
|
+
width: Optional[int] = None
|
|
36
|
+
height: Optional[int] = None
|
|
37
|
+
auto_analyze: bool = True
|
|
38
|
+
change_threshold: float = 0.05
|
|
39
|
+
blur_detection: bool = True
|
|
40
|
+
max_session_minutes: int = 60
|
|
41
|
+
privacy_mode: bool = True
|
|
42
|
+
suppress_notifications: bool = True
|
|
43
|
+
suppress_system_audio: bool = False
|
|
44
|
+
cursor_capture: bool = True
|
|
45
|
+
annotations_enabled: bool = True
|
|
46
|
+
saved_frames_max: int = 20
|
|
47
|
+
context_window_frames: int = 5
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
50
|
+
return {
|
|
51
|
+
"mode": self.mode.value,
|
|
52
|
+
"fps": self.fps,
|
|
53
|
+
"quality": self.quality,
|
|
54
|
+
"max_frames": self.max_frames,
|
|
55
|
+
"width": self.width,
|
|
56
|
+
"height": self.height,
|
|
57
|
+
"auto_analyze": self.auto_analyze,
|
|
58
|
+
"change_threshold": self.change_threshold,
|
|
59
|
+
"blur_detection": self.blur_detection,
|
|
60
|
+
"max_session_minutes": self.max_session_minutes,
|
|
61
|
+
"privacy_mode": self.privacy_mode,
|
|
62
|
+
"suppress_notifications": self.suppress_notifications,
|
|
63
|
+
"cursor_capture": self.cursor_capture,
|
|
64
|
+
"annotations_enabled": self.annotations_enabled,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class CaptureEngine:
|
|
69
|
+
"""Server-side capture controller.
|
|
70
|
+
|
|
71
|
+
The actual screen capture runs in the browser via getDisplayMedia().
|
|
72
|
+
This engine:
|
|
73
|
+
1. Generates the frontend capture config/instructions
|
|
74
|
+
2. Processes incoming frames from the browser
|
|
75
|
+
3. Detects capture capability and falls back gracefully
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, config: CaptureConfig):
|
|
79
|
+
self.config = config
|
|
80
|
+
self.paused = False
|
|
81
|
+
self._initialized = False
|
|
82
|
+
self._frame_callback: Optional[Callable] = None
|
|
83
|
+
self._stats = {
|
|
84
|
+
"frames_received": 0,
|
|
85
|
+
"frames_dropped": 0,
|
|
86
|
+
"bytes_received": 0,
|
|
87
|
+
"errors": 0,
|
|
88
|
+
"started_at": None,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async def initialize(self):
|
|
92
|
+
self._initialized = True
|
|
93
|
+
self._stats["started_at"] = time.time()
|
|
94
|
+
|
|
95
|
+
async def cleanup(self):
|
|
96
|
+
self._initialized = False
|
|
97
|
+
self._frame_callback = None
|
|
98
|
+
|
|
99
|
+
def get_capture_config(self) -> Dict[str, Any]:
|
|
100
|
+
"""Return config dict for the frontend to initialize getDisplayMedia."""
|
|
101
|
+
display_surface = "monitor"
|
|
102
|
+
if self.config.mode == CaptureMode.TAB:
|
|
103
|
+
display_surface = "browser"
|
|
104
|
+
elif self.config.mode == CaptureMode.WINDOW:
|
|
105
|
+
display_surface = "window"
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"video": {
|
|
109
|
+
"displaySurface": display_surface,
|
|
110
|
+
"logicalSurface": True,
|
|
111
|
+
"cursor": "always" if self.config.cursor_capture else "never",
|
|
112
|
+
"noiseSuppression": not self.config.suppress_system_audio,
|
|
113
|
+
"suppressLocalAudioPlayback": self.config.suppress_system_audio,
|
|
114
|
+
"selfBrowserSurface": "include",
|
|
115
|
+
"systemAudio": "exclude" if self.config.suppress_system_audio else "include",
|
|
116
|
+
"preferCurrentTab": self.config.mode == CaptureMode.TAB,
|
|
117
|
+
},
|
|
118
|
+
"audio": self.config.mode in (
|
|
119
|
+
CaptureMode.AUDIO_VIDEO, CaptureMode.AUDIO_ONLY),
|
|
120
|
+
"videoConstraints": {
|
|
121
|
+
"width": {"ideal": self.config.width or 1920},
|
|
122
|
+
"height": {"ideal": self.config.height or 1080},
|
|
123
|
+
"frameRate": {"ideal": self.config.fps, "max": self.config.fps * 2},
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
def get_capability_report(self) -> Dict[str, Any]:
|
|
128
|
+
"""Report what capture modes the frontend should attempt."""
|
|
129
|
+
return {
|
|
130
|
+
"display_api": True,
|
|
131
|
+
"media_api": True,
|
|
132
|
+
"audio_capture": self.config.mode in (
|
|
133
|
+
CaptureMode.AUDIO_VIDEO, CaptureMode.AUDIO_ONLY),
|
|
134
|
+
"tab_capture": True,
|
|
135
|
+
"window_capture": True,
|
|
136
|
+
"frame_rate": self.config.fps,
|
|
137
|
+
"max_resolution": f"{self.config.width or 1920}x{self.config.height or 1080}",
|
|
138
|
+
"browser_requirements": "getDisplayMedia() requires HTTPS or localhost",
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
def process_incoming_frame(self, frame_b64: str,
|
|
142
|
+
timestamp: Optional[float] = None,
|
|
143
|
+
metadata: Optional[Dict] = None) -> Dict[str, Any]:
|
|
144
|
+
"""Process a base64 frame received from the browser."""
|
|
145
|
+
if self.paused:
|
|
146
|
+
self._stats["frames_dropped"] += 1
|
|
147
|
+
return {"processed": False, "reason": "paused"}
|
|
148
|
+
|
|
149
|
+
self._stats["frames_received"] += 1
|
|
150
|
+
self._stats["bytes_received"] += len(frame_b64)
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
"processed": True,
|
|
154
|
+
"frame_index": self._stats["frames_received"],
|
|
155
|
+
"size_bytes": len(frame_b64),
|
|
156
|
+
"timestamp": timestamp or time.time(),
|
|
157
|
+
"metadata": metadata or {},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def stats(self) -> Dict[str, Any]:
|
|
162
|
+
elapsed = 0.0
|
|
163
|
+
if self._stats["started_at"]:
|
|
164
|
+
elapsed = time.time() - self._stats["started_at"]
|
|
165
|
+
return {
|
|
166
|
+
**self._stats,
|
|
167
|
+
"elapsed_seconds": round(elapsed, 2),
|
|
168
|
+
"avg_fps": round(
|
|
169
|
+
self._stats["frames_received"] / max(elapsed, 0.01), 2
|
|
170
|
+
),
|
|
171
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT v0.8.a — Vision Context Engine.
|
|
3
|
+
|
|
4
|
+
Fuses screen frame + code context + project structure + chat history
|
|
5
|
+
into a single VisualContext object for AI analysis.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class VisualContext:
|
|
18
|
+
"""Complete visual context for AI analysis."""
|
|
19
|
+
frame_b64: str
|
|
20
|
+
frame_meta: Dict[str, Any]
|
|
21
|
+
annotations: List[Dict[str, Any]]
|
|
22
|
+
code_context: Optional[Dict[str, Any]] = None
|
|
23
|
+
project_context: Optional[Dict[str, Any]] = None
|
|
24
|
+
chat_history: List[Dict[str, str]] = field(default_factory=list)
|
|
25
|
+
pointer_telemetry: Optional[Dict[str, Any]] = None
|
|
26
|
+
before_frame_b64: Optional[str] = None
|
|
27
|
+
before_meta: Optional[Dict[str, Any]] = None
|
|
28
|
+
analysis_prompt: str = ""
|
|
29
|
+
created_at: float = 0.0
|
|
30
|
+
|
|
31
|
+
def __post_init__(self):
|
|
32
|
+
if self.created_at == 0.0:
|
|
33
|
+
self.created_at = time.time()
|
|
34
|
+
|
|
35
|
+
def to_dict(self, include_frames: bool = False) -> Dict[str, Any]:
|
|
36
|
+
result = {
|
|
37
|
+
"frame_meta": self.frame_meta,
|
|
38
|
+
"annotations": self.annotations,
|
|
39
|
+
"code_context": self.code_context,
|
|
40
|
+
"project_context": self.project_context,
|
|
41
|
+
"chat_history": self.chat_history[-10:],
|
|
42
|
+
"pointer_telemetry": self.pointer_telemetry,
|
|
43
|
+
"analysis_prompt": self.analysis_prompt,
|
|
44
|
+
"created_at": self.created_at,
|
|
45
|
+
"has_frame": bool(self.frame_b64),
|
|
46
|
+
"has_before_frame": bool(self.before_frame_b64),
|
|
47
|
+
}
|
|
48
|
+
if include_frames:
|
|
49
|
+
result["frame_b64"] = self.frame_b64
|
|
50
|
+
result["before_frame_b64"] = self.before_frame_b64
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
def to_ai_messages(self) -> List[Dict[str, Any]]:
|
|
54
|
+
"""Build the message payload for the AI vision model."""
|
|
55
|
+
messages = []
|
|
56
|
+
system_parts = [
|
|
57
|
+
"You are CAT Vision Agent. Analyze the screen state and provide "
|
|
58
|
+
"actionable insights. Be specific about UI elements, errors, and "
|
|
59
|
+
"suggested fixes."
|
|
60
|
+
]
|
|
61
|
+
if self.code_context:
|
|
62
|
+
system_parts.append(
|
|
63
|
+
f"\nActive file: {self.code_context.get('file_path', 'unknown')}"
|
|
64
|
+
)
|
|
65
|
+
if self.code_context.get("cursor_line"):
|
|
66
|
+
system_parts.append(
|
|
67
|
+
f"Cursor at line: {self.code_context['cursor_line']}"
|
|
68
|
+
)
|
|
69
|
+
if self.project_context:
|
|
70
|
+
system_parts.append(
|
|
71
|
+
f"Project type: {self.project_context.get('type', 'unknown')}"
|
|
72
|
+
)
|
|
73
|
+
messages.append({"role": "system", "content": "\n".join(system_parts)})
|
|
74
|
+
|
|
75
|
+
user_parts = []
|
|
76
|
+
if self.analysis_prompt:
|
|
77
|
+
user_parts.append(self.analysis_prompt)
|
|
78
|
+
else:
|
|
79
|
+
user_parts.append(
|
|
80
|
+
"Analyze this screen capture. Describe what you see, identify "
|
|
81
|
+
"any issues, and suggest next steps."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if self.pointer_telemetry:
|
|
85
|
+
pt = self.pointer_telemetry
|
|
86
|
+
if pt.get("active_element"):
|
|
87
|
+
user_parts.append(
|
|
88
|
+
f"\nFocused element: {pt['active_element']}"
|
|
89
|
+
)
|
|
90
|
+
if pt.get("cursor_moving"):
|
|
91
|
+
user_parts.append("\nUser is actively moving the cursor.")
|
|
92
|
+
|
|
93
|
+
if self.annotations:
|
|
94
|
+
ann_desc = []
|
|
95
|
+
for ann in self.annotations:
|
|
96
|
+
tool = ann.get("tool", "")
|
|
97
|
+
if tool == "text":
|
|
98
|
+
ann_desc.append(f"Text annotation: {ann.get('text', '')}")
|
|
99
|
+
elif tool == "arrow":
|
|
100
|
+
ann_desc.append("Arrow annotation")
|
|
101
|
+
elif tool == "highlight":
|
|
102
|
+
ann_desc.append("Highlighted region")
|
|
103
|
+
else:
|
|
104
|
+
ann_desc.append(f"{tool} annotation")
|
|
105
|
+
user_parts.append(
|
|
106
|
+
"\nUser annotations: " + "; ".join(ann_desc)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
user_content = [
|
|
110
|
+
{"type": "text", "text": "\n".join(user_parts)}
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
if self.frame_b64:
|
|
114
|
+
mime = "image/png"
|
|
115
|
+
user_content.append({
|
|
116
|
+
"type": "image_url",
|
|
117
|
+
"image_url": {
|
|
118
|
+
"url": f"data:{mime};base64,{self.frame_b64}",
|
|
119
|
+
"detail": "high",
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if self.before_frame_b64:
|
|
124
|
+
user_content.append({
|
|
125
|
+
"type": "text",
|
|
126
|
+
"text": "\n--- BEFORE state (for comparison) ---"
|
|
127
|
+
})
|
|
128
|
+
user_content.append({
|
|
129
|
+
"type": "image_url",
|
|
130
|
+
"image_url": {
|
|
131
|
+
"url": f"data:image/png;base64,{self.before_frame_b64}",
|
|
132
|
+
"detail": "high",
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
messages.append({"role": "user", "content": user_content})
|
|
137
|
+
return messages
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class VisionContextEngine:
|
|
141
|
+
"""Builds VisualContext from multiple sources."""
|
|
142
|
+
|
|
143
|
+
def __init__(self):
|
|
144
|
+
self._code_context: Optional[Dict[str, Any]] = None
|
|
145
|
+
self._project_context: Optional[Dict[str, Any]] = None
|
|
146
|
+
self._chat_history: List[Dict[str, str]] = []
|
|
147
|
+
self._max_chat = 20
|
|
148
|
+
|
|
149
|
+
def set_code_context(self, file_path: str = "", content: str = "",
|
|
150
|
+
cursor_line: int = 0, language: str = "",
|
|
151
|
+
diagnostics: Optional[List[str]] = None):
|
|
152
|
+
self._code_context = {
|
|
153
|
+
"file_path": file_path,
|
|
154
|
+
"content": content[:5000] if content else "",
|
|
155
|
+
"cursor_line": cursor_line,
|
|
156
|
+
"language": language,
|
|
157
|
+
"diagnostics": diagnostics or [],
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def set_project_context(self, project_type: str = "",
|
|
161
|
+
structure: Optional[Dict] = None,
|
|
162
|
+
dependencies: Optional[List[str]] = None):
|
|
163
|
+
self._project_context = {
|
|
164
|
+
"type": project_type,
|
|
165
|
+
"structure": structure or {},
|
|
166
|
+
"dependencies": dependencies or [],
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
def add_chat_turn(self, role: str, content: str):
|
|
170
|
+
self._chat_history.append({"role": role, "content": content[:2000]})
|
|
171
|
+
if len(self._chat_history) > self._max_chat:
|
|
172
|
+
self._chat_history = self._chat_history[-self._max_chat:]
|
|
173
|
+
|
|
174
|
+
def build_context(self, frame_b64: str,
|
|
175
|
+
frame_meta: Optional[Dict] = None,
|
|
176
|
+
annotations: Optional[List[Dict]] = None,
|
|
177
|
+
pointer_telemetry: Optional[Dict] = None,
|
|
178
|
+
analysis_prompt: str = "",
|
|
179
|
+
before_frame_b64: Optional[str] = None,
|
|
180
|
+
before_meta: Optional[Dict] = None) -> VisualContext:
|
|
181
|
+
return VisualContext(
|
|
182
|
+
frame_b64=frame_b64,
|
|
183
|
+
frame_meta=frame_meta or {},
|
|
184
|
+
annotations=annotations or [],
|
|
185
|
+
code_context=self._code_context,
|
|
186
|
+
project_context=self._project_context,
|
|
187
|
+
chat_history=list(self._chat_history),
|
|
188
|
+
pointer_telemetry=pointer_telemetry,
|
|
189
|
+
analysis_prompt=analysis_prompt,
|
|
190
|
+
before_frame_b64=before_frame_b64,
|
|
191
|
+
before_meta=before_meta,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def build_visual_debug_prompt(self, error_message: str = "",
|
|
195
|
+
user_intent: str = "") -> str:
|
|
196
|
+
parts = [
|
|
197
|
+
"VISUAL DEBUG SESSION — Analyze the current screen state.",
|
|
198
|
+
"The user is experiencing an issue or wants to understand what's on screen.",
|
|
199
|
+
]
|
|
200
|
+
if error_message:
|
|
201
|
+
parts.append(f"\nError reported: {error_message}")
|
|
202
|
+
if user_intent:
|
|
203
|
+
parts.append(f"\nUser intent: {user_intent}")
|
|
204
|
+
parts.append(
|
|
205
|
+
"\nProvide:\n"
|
|
206
|
+
"1. What you see on screen (UI elements, text, state)\n"
|
|
207
|
+
"2. Any errors, warnings, or anomalies\n"
|
|
208
|
+
"3. Root cause hypothesis\n"
|
|
209
|
+
"4. Specific fix suggestion with code if applicable\n"
|
|
210
|
+
"5. Before/after comparison if previous frame is available"
|
|
211
|
+
)
|
|
212
|
+
return "\n".join(parts)
|
|
213
|
+
|
|
214
|
+
def build_code_review_prompt(self, file_path: str = "",
|
|
215
|
+
code: str = "") -> str:
|
|
216
|
+
parts = [
|
|
217
|
+
"CODE-VISUAL REVIEW — The screen shows the current code state.",
|
|
218
|
+
"Review the visible code and provide feedback.",
|
|
219
|
+
]
|
|
220
|
+
if file_path:
|
|
221
|
+
parts.append(f"\nFile: {file_path}")
|
|
222
|
+
if code:
|
|
223
|
+
parts.append(f"\nCode preview:\n```\n{code[:3000]}\n```")
|
|
224
|
+
parts.append(
|
|
225
|
+
"\nAnalyze:\n"
|
|
226
|
+
"1. Code quality and correctness\n"
|
|
227
|
+
"2. Visual rendering issues (if web app)\n"
|
|
228
|
+
"3. UI/UX observations\n"
|
|
229
|
+
"4. Suggested improvements"
|
|
230
|
+
)
|
|
231
|
+
return "\n".join(parts)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT v0.8.a — Screen → Code Correlation (spec 20).
|
|
3
|
+
|
|
4
|
+
Maps a visual annotation/cursor region to likely project files by:
|
|
5
|
+
- DOM/preview context (if live preview is running)
|
|
6
|
+
- filename / component heuristics
|
|
7
|
+
- workspace search for component names near the annotation label
|
|
8
|
+
|
|
9
|
+
Never hallucinates an exact source location from pixels alone.
|
|
10
|
+
Returns a ranked list with confidence + evidence.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _norm_annotation_label(ann: Dict[str, Any]) -> str:
|
|
22
|
+
return (ann.get("text") or ann.get("label") or ann.get("description") or "").strip()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _guess_component_from_label(label: str) -> List[str]:
|
|
26
|
+
"""Extract likely component/file hints from a short label like 'Submit button glitches'."""
|
|
27
|
+
if not label:
|
|
28
|
+
return []
|
|
29
|
+
words = re.findall(r"[A-Za-z][A-Za-z0-9_-]*", label)
|
|
30
|
+
# keep capitalized or component-like words, also "button", "form", "submit"
|
|
31
|
+
hints = []
|
|
32
|
+
for w in words:
|
|
33
|
+
lw = w.lower()
|
|
34
|
+
if len(w) >= 3 and lw not in {"this", "that", "when", "with", "glitches", "broken", "jumps"}:
|
|
35
|
+
hints.append(w)
|
|
36
|
+
return hints[:8]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _search_workspace(hints: List[str], root: str, max_hits: int = 12) -> List[Dict[str, Any]]:
|
|
40
|
+
"""Cheap filename + grep search. Returns [{path, reason, score}]."""
|
|
41
|
+
if not root or not os.path.isdir(root):
|
|
42
|
+
return []
|
|
43
|
+
root_p = Path(root)
|
|
44
|
+
results: List[Dict[str, Any]] = []
|
|
45
|
+
# filename scan
|
|
46
|
+
all_files = []
|
|
47
|
+
for p in root_p.rglob("*"):
|
|
48
|
+
if p.is_file() and p.suffix.lower() in {".tsx",".ts",".jsx",".js",".py",".html",".css",".vue",".svelte"}:
|
|
49
|
+
if any(part.startswith(".") or part == "node_modules" or part == "__pycache__" for part in p.parts):
|
|
50
|
+
continue
|
|
51
|
+
all_files.append(p)
|
|
52
|
+
if len(all_files) > 8000:
|
|
53
|
+
break
|
|
54
|
+
for hint in hints:
|
|
55
|
+
hl = hint.lower()
|
|
56
|
+
for f in all_files:
|
|
57
|
+
name_l = f.name.lower()
|
|
58
|
+
if hl in name_l or hl.replace("-","") in name_l.replace("-",""):
|
|
59
|
+
results.append({"path": str(f.relative_to(root_p)), "reason": f"filename contains '{hint}'", "score": 0.7})
|
|
60
|
+
if len(results) >= max_hits:
|
|
61
|
+
return results
|
|
62
|
+
# content grep for component names (cheap, capped)
|
|
63
|
+
for hint in hints[:3]:
|
|
64
|
+
hl = hint.lower()
|
|
65
|
+
if len(hl) < 3:
|
|
66
|
+
continue
|
|
67
|
+
for f in all_files[:1200]:
|
|
68
|
+
try:
|
|
69
|
+
text = f.read_text(encoding="utf-8", errors="ignore")[:20000]
|
|
70
|
+
except Exception:
|
|
71
|
+
continue
|
|
72
|
+
if hl in text.lower():
|
|
73
|
+
# boost if it's a component definition
|
|
74
|
+
score = 0.6
|
|
75
|
+
if re.search(rf"(class|function|const)\s+{re.escape(hint)}\b", text):
|
|
76
|
+
score = 0.85
|
|
77
|
+
elif re.search(rf"<{re.escape(hint)}\b", text):
|
|
78
|
+
score = 0.75
|
|
79
|
+
results.append({"path": str(f.relative_to(root_p)), "reason": f"mentions '{hint}'", "score": score})
|
|
80
|
+
if len(results) >= max_hits:
|
|
81
|
+
return results
|
|
82
|
+
# dedupe, keep highest score per path
|
|
83
|
+
best: Dict[str, Dict] = {}
|
|
84
|
+
for r in results:
|
|
85
|
+
prev = best.get(r["path"])
|
|
86
|
+
if prev is None or r["score"] > prev["score"]:
|
|
87
|
+
best[r["path"]] = r
|
|
88
|
+
ranked = sorted(best.values(), key=lambda r: r["score"], reverse=True)
|
|
89
|
+
return ranked[:max_hits]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def correlate(
|
|
93
|
+
annotations: List[Dict[str, Any]],
|
|
94
|
+
cursor: Optional[Dict[str, Any]] = None,
|
|
95
|
+
preview_context: Optional[Dict[str, Any]] = None,
|
|
96
|
+
workspace_root: Optional[str] = None,
|
|
97
|
+
) -> Dict[str, Any]:
|
|
98
|
+
"""
|
|
99
|
+
Returns {
|
|
100
|
+
"has_mapping": bool,
|
|
101
|
+
"candidates": [{path, reason, score}],
|
|
102
|
+
"preview_hint": str | None,
|
|
103
|
+
"notes": str # honest about uncertainty
|
|
104
|
+
}
|
|
105
|
+
"""
|
|
106
|
+
# preview / DOM hint is strongest — when live preview controller knows the entry URL + component
|
|
107
|
+
preview_hint = None
|
|
108
|
+
candidates: List[Dict[str, Any]] = []
|
|
109
|
+
notes_parts: List[str] = []
|
|
110
|
+
|
|
111
|
+
if preview_context:
|
|
112
|
+
# preview_context may contain entry file / component map produced by browser/preview.py
|
|
113
|
+
entry = preview_context.get("entry") or preview_context.get("url") or ""
|
|
114
|
+
if entry:
|
|
115
|
+
preview_hint = entry
|
|
116
|
+
candidates.append({"path": entry, "reason": "live preview entry", "score": 0.65})
|
|
117
|
+
dom = preview_context.get("dom_match") or preview_context.get("selector") or ""
|
|
118
|
+
if dom:
|
|
119
|
+
notes_parts.append(f"DOM context: {dom}")
|
|
120
|
+
|
|
121
|
+
# annotation labels → hints → workspace search
|
|
122
|
+
all_hints: List[str] = []
|
|
123
|
+
for ann in (annotations or []):
|
|
124
|
+
label = _norm_annotation_label(ann)
|
|
125
|
+
hints = _guess_component_from_label(label)
|
|
126
|
+
tool = ann.get("tool", "")
|
|
127
|
+
if tool:
|
|
128
|
+
hints.append(tool)
|
|
129
|
+
all_hints.extend(hints)
|
|
130
|
+
|
|
131
|
+
# also from cursor active_element
|
|
132
|
+
if cursor and cursor.get("active_element"):
|
|
133
|
+
all_hints.append(str(cursor["active_element"])[:40])
|
|
134
|
+
|
|
135
|
+
# dedupe hints
|
|
136
|
+
seen = set()
|
|
137
|
+
uniq_hints: List[str] = []
|
|
138
|
+
for h in all_hints:
|
|
139
|
+
hl = h.lower()
|
|
140
|
+
if hl not in seen:
|
|
141
|
+
seen.add(hl)
|
|
142
|
+
uniq_hints.append(h)
|
|
143
|
+
|
|
144
|
+
if uniq_hints and workspace_root:
|
|
145
|
+
hits = _search_workspace(uniq_hints, workspace_root, max_hits=10)
|
|
146
|
+
candidates.extend(hits)
|
|
147
|
+
|
|
148
|
+
# sort + dedupe candidates
|
|
149
|
+
best: Dict[str, Dict] = {}
|
|
150
|
+
for c in candidates:
|
|
151
|
+
prev = best.get(c["path"])
|
|
152
|
+
if prev is None or c["score"] > prev["score"]:
|
|
153
|
+
best[c["path"]] = c
|
|
154
|
+
ranked = sorted(best.values(), key=lambda c: c["score"], reverse=True)
|
|
155
|
+
|
|
156
|
+
has_mapping = bool(ranked and ranked[0]["score"] >= 0.6)
|
|
157
|
+
if not ranked:
|
|
158
|
+
notes_parts.append("No code mapping could be inferred from the current visual context.")
|
|
159
|
+
notes_parts.append("Try adding a short annotation label (e.g. 'Submit button') or opening the preview so CAT can see the component tree.")
|
|
160
|
+
elif not has_mapping:
|
|
161
|
+
notes_parts.append("Weak mapping only — candidates are guesses, not exact source locations.")
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
"has_mapping": has_mapping,
|
|
165
|
+
"candidates": ranked[:8],
|
|
166
|
+
"preview_hint": preview_hint,
|
|
167
|
+
"hints_used": uniq_hints[:10],
|
|
168
|
+
"notes": " ".join(notes_parts),
|
|
169
|
+
}
|