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,424 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT UI — attachment + smart-paste chips, living in the composer's
|
|
3
|
+
AttachmentBar. Renders state (what's attached, what's collapsed) and
|
|
4
|
+
dispatches events (AttachmentAdded/Removed, PasteCollapsed/Expanded);
|
|
5
|
+
StickyComposer/CCTApp decide what those events mean.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
if __name__ == "__main__":
|
|
9
|
+
print("This is a library file and is not meant to be run directly.")
|
|
10
|
+
import sys
|
|
11
|
+
sys.exit(1)
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
TEXTUAL_AVAILABLE = True
|
|
16
|
+
try:
|
|
17
|
+
from textual.containers import Horizontal
|
|
18
|
+
except Exception:
|
|
19
|
+
TEXTUAL_AVAILABLE = False
|
|
20
|
+
|
|
21
|
+
from .widgets import Chip, TEXTUAL_AVAILABLE as _WIDGETS_OK
|
|
22
|
+
from .events import (AttachmentAdded, AttachmentRemoved, PasteCollapsed,
|
|
23
|
+
PasteExpanded, AttachmentChipDoubleClicked)
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from .. import attachments as _core
|
|
27
|
+
except Exception: # pragma: no cover — import-time fallback
|
|
28
|
+
_core = None
|
|
29
|
+
|
|
30
|
+
TEXTUAL_AVAILABLE = TEXTUAL_AVAILABLE and _WIDGETS_OK
|
|
31
|
+
|
|
32
|
+
# Paste thresholds from the redesign brief: under these a paste inserts
|
|
33
|
+
# normally into the editor; at/above either one, collapse into a chip.
|
|
34
|
+
PASTE_COLLAPSE_THRESHOLD = 10
|
|
35
|
+
PASTE_WORD_THRESHOLD = 100
|
|
36
|
+
|
|
37
|
+
# Favorite folders for the attach browser (v0.7.8.1): a tiny, honest
|
|
38
|
+
# store — just a JSON list of folder paths the user starred in the
|
|
39
|
+
# Attach panel's Favorites tab.
|
|
40
|
+
FAVORITES_FILE = os.path.join(os.path.expanduser("~"), ".cct_attach_favorites.json")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def favorite_folders() -> list[str]:
|
|
44
|
+
"""Starred folders for the Attach browser. Never raises."""
|
|
45
|
+
try:
|
|
46
|
+
if os.path.exists(FAVORITES_FILE):
|
|
47
|
+
with open(FAVORITES_FILE, "r", encoding="utf-8") as f:
|
|
48
|
+
import json
|
|
49
|
+
data = json.load(f)
|
|
50
|
+
if isinstance(data, list):
|
|
51
|
+
return [p for p in data if isinstance(p, str) and os.path.isdir(p)]
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def toggle_favorite_folder(path: str) -> bool:
|
|
58
|
+
"""Star/unstar a folder. Returns True when it is now a favorite."""
|
|
59
|
+
path = os.path.normpath(path)
|
|
60
|
+
favs = favorite_folders()
|
|
61
|
+
if path in favs:
|
|
62
|
+
favs = [p for p in favs if p != path]
|
|
63
|
+
added = False
|
|
64
|
+
else:
|
|
65
|
+
favs.append(path)
|
|
66
|
+
added = True
|
|
67
|
+
try:
|
|
68
|
+
import json
|
|
69
|
+
with open(FAVORITES_FILE, "w", encoding="utf-8") as f:
|
|
70
|
+
json.dump(favs, f, indent=2)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
return added
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def list_drives() -> list[str]:
|
|
77
|
+
"""Windows drive letters that exist (A:, C:, ...). Empty elsewhere."""
|
|
78
|
+
try:
|
|
79
|
+
import string
|
|
80
|
+
return [f"{c}:\\" for c in string.ascii_uppercase
|
|
81
|
+
if os.path.exists(f"{c}:\\")]
|
|
82
|
+
except Exception:
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def paths_from_paste(text):
|
|
87
|
+
"""Drag & drop / paste → attachment paths (v0.7.6 Patch 1, Fix 5;
|
|
88
|
+
hardened v0.8.1).
|
|
89
|
+
|
|
90
|
+
Textual 8.2.8 has no OS file-drop event, but dragging a file into a
|
|
91
|
+
Windows terminal inserts its path into the input buffer anyway —
|
|
92
|
+
which arrives here as a paste. So "drag & drop" is realised as
|
|
93
|
+
path-paste detection, and multi-file drops are covered too:
|
|
94
|
+
Explorer copies/drops NUL-separated paths (`\x00`); some hosts use
|
|
95
|
+
CRLF or quoted single-line forms.
|
|
96
|
+
|
|
97
|
+
Returns a list of existing absolute paths when the whole paste is
|
|
98
|
+
nothing but paths (one per line / NUL-separated, optional quotes),
|
|
99
|
+
else None so the caller falls back to a normal text paste. A path
|
|
100
|
+
must exist on disk — a stale/typo'd path is treated as prose."""
|
|
101
|
+
if not text:
|
|
102
|
+
return None
|
|
103
|
+
normalized = text.replace("\x00", "\n").replace("\r\n", "\n")
|
|
104
|
+
paths = []
|
|
105
|
+
for part in normalized.split("\n"):
|
|
106
|
+
p = part.strip().strip('"').strip("'").strip()
|
|
107
|
+
if not p:
|
|
108
|
+
continue
|
|
109
|
+
# tolerate the "file:///C:/..." URI form some apps put on the clipboard
|
|
110
|
+
if p.lower().startswith("file:///"):
|
|
111
|
+
from urllib.parse import unquote, urlparse
|
|
112
|
+
try:
|
|
113
|
+
p = unquote(urlparse(p).path).lstrip("/") or p
|
|
114
|
+
if len(p) > 1 and p[1] != ":" and os.name == "nt":
|
|
115
|
+
p = "C:" + p # unlikely; keep best-effort
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
if not os.path.isabs(p) or not os.path.lexists(p):
|
|
119
|
+
return None
|
|
120
|
+
paths.append(os.path.normpath(p))
|
|
121
|
+
return paths or None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def attachment_kind(path):
|
|
125
|
+
"""Fix 6 (chat attachment cards): an icon + kind label for an
|
|
126
|
+
attachment path, chosen purely from its extension. Presentation
|
|
127
|
+
only — the AI-side reading/analysis (Fix 7) lives in aicore."""
|
|
128
|
+
ext = os.path.splitext(path)[1].lower()
|
|
129
|
+
if ext in {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg", ".ico"}:
|
|
130
|
+
return "\U0001f5bc\ufe0f", "image"
|
|
131
|
+
if ext == ".pdf":
|
|
132
|
+
return "\U0001f4d5", "pdf"
|
|
133
|
+
if ext in {".zip", ".rar", ".7z", ".tar", ".gz", ".bz2"}:
|
|
134
|
+
return "\U0001f4e6", "archive"
|
|
135
|
+
if ext in {".csv", ".tsv", ".xlsx", ".xls", ".json", ".xml", ".yaml", ".yml",
|
|
136
|
+
".toml", ".ini", ".conf", ".env"}:
|
|
137
|
+
return "\U0001f4ca", "data"
|
|
138
|
+
if ext == ".md":
|
|
139
|
+
return "\U0001f4dd", "markdown"
|
|
140
|
+
if ext in {".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".c", ".cpp", ".h", ".hpp",
|
|
141
|
+
".cs", ".go", ".rs", ".rb", ".php", ".sh", ".ps1", ".bat", ".html", ".css",
|
|
142
|
+
".sql", ".lua", ".r", ".swift", ".kt"}:
|
|
143
|
+
return "\U0001f4dc", "code"
|
|
144
|
+
if ext in {".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac"}:
|
|
145
|
+
return "\U0001f3b5", "audio"
|
|
146
|
+
if ext in {".mp4", ".mkv", ".avi", ".mov", ".webm", ".wmv"}:
|
|
147
|
+
return "\U0001f3ac", "video"
|
|
148
|
+
return "\U0001f4c4", "file"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def format_size(path):
|
|
152
|
+
try:
|
|
153
|
+
size = os.path.getsize(path)
|
|
154
|
+
except Exception:
|
|
155
|
+
return ""
|
|
156
|
+
if size < 1024:
|
|
157
|
+
return f"{size} B"
|
|
158
|
+
if size < 1024 ** 2:
|
|
159
|
+
return f"{size / 1024:.1f} KB"
|
|
160
|
+
if size < 1024 ** 3:
|
|
161
|
+
return f"{size / 1024 ** 2:.1f} MB"
|
|
162
|
+
return f"{size / 1024 ** 3:.1f} GB"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _size_label(size):
|
|
166
|
+
try:
|
|
167
|
+
size = int(size or 0)
|
|
168
|
+
except Exception:
|
|
169
|
+
size = 0
|
|
170
|
+
if size < 1024:
|
|
171
|
+
return f"{size} B"
|
|
172
|
+
if size < 1024 ** 2:
|
|
173
|
+
return f"{size / 1024:.1f} KB"
|
|
174
|
+
if size < 1024 ** 3:
|
|
175
|
+
return f"{size / 1024 ** 2:.1f} MB"
|
|
176
|
+
return f"{size / 1024 ** 3:.1f} GB"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _status_label(att):
|
|
180
|
+
"""v0.7.8.1: the status half of an attachment chip — 'reading…'
|
|
181
|
+
while extraction runs, 'kind · size' when ready, the honest error
|
|
182
|
+
when it failed. The color lives in the chip markup, not here."""
|
|
183
|
+
status = getattr(att, "extraction_status", None)
|
|
184
|
+
if status == _core.STATUS_READING:
|
|
185
|
+
return "reading\u2026"
|
|
186
|
+
if status == _core.STATUS_READY:
|
|
187
|
+
label = _core.KIND_LABELS.get(getattr(att, "kind", ""), "File")
|
|
188
|
+
return f"{label.lower()} \u00b7 {_size_label(getattr(att, 'size', 0))}"
|
|
189
|
+
if status == _core.STATUS_FAILED:
|
|
190
|
+
return getattr(att, "error", None) or "could not be read"
|
|
191
|
+
return "reading\u2026"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _status_color(att):
|
|
195
|
+
"""Dim for pending/ready, the theme's error color for failures."""
|
|
196
|
+
if getattr(att, "extraction_status", None) == _core.STATUS_FAILED:
|
|
197
|
+
from . import theme_css
|
|
198
|
+
return theme_css.current_hex("error")
|
|
199
|
+
from . import theme_css
|
|
200
|
+
return theme_css.current_hex("text-faint")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _vision_warning(att):
|
|
204
|
+
"""CAT automatically extracts OCR, multimodal features, and metadata
|
|
205
|
+
for all attached images and files across all models. Returns None so no
|
|
206
|
+
false warnings are displayed."""
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _chip_label(att):
|
|
211
|
+
"""One chip's full markup: icon + name + status + weak-model note."""
|
|
212
|
+
from . import theme_css
|
|
213
|
+
icon, _k = attachment_kind(att.path)
|
|
214
|
+
name = att.name
|
|
215
|
+
label = (f"{icon} [{theme_css.current_hex('text')}]{name}[/]"
|
|
216
|
+
f"[{_status_color(att)}]\u00b7{_status_label(att)}[/]")
|
|
217
|
+
warn = _vision_warning(att)
|
|
218
|
+
if warn:
|
|
219
|
+
label += f"[{theme_css.current_hex('warning')}] \u26a0 {warn}[/]"
|
|
220
|
+
return label
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
if TEXTUAL_AVAILABLE:
|
|
224
|
+
|
|
225
|
+
class AttachmentBar(Horizontal):
|
|
226
|
+
"""Row of attachment + paste chips, living inside the composer
|
|
227
|
+
card under the editor. Empty (height: auto collapses to 0) until
|
|
228
|
+
something is attached.
|
|
229
|
+
|
|
230
|
+
Holds two kinds of chips:
|
|
231
|
+
- file attachments (label = filename; delete removes it)
|
|
232
|
+
- collapsed pastes (label = "[Pasted ~N lines]"; clicking the
|
|
233
|
+
label re-expands the full text via PasteExpanded; delete
|
|
234
|
+
discards it for good)
|
|
235
|
+
|
|
236
|
+
v0.7.8.1: every attached path is normalized into an Attachment
|
|
237
|
+
object (calc_terminal/attachments.py) the moment it's added.
|
|
238
|
+
Extraction runs off the UI thread; the chip shows 'reading…'
|
|
239
|
+
until it resolves to 'ready' (kind · size) or 'failed' (honest
|
|
240
|
+
error). The Attachment objects travel with the message via
|
|
241
|
+
pop_attachment_objects() — the AI pipeline receives real
|
|
242
|
+
objects, never bare path strings.
|
|
243
|
+
|
|
244
|
+
Every state change posts the matching event instead of being
|
|
245
|
+
polled — StickyComposer/CCTApp react to those, they never reach
|
|
246
|
+
back into this bar's internals.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
DEFAULT_CSS = """
|
|
250
|
+
AttachmentBar {
|
|
251
|
+
height: auto;
|
|
252
|
+
width: 100%;
|
|
253
|
+
padding: 0 1;
|
|
254
|
+
margin: 0;
|
|
255
|
+
display: none;
|
|
256
|
+
}
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
def __init__(self):
|
|
260
|
+
super().__init__(id="cct-chips")
|
|
261
|
+
self._attachments = {} # chip_id -> Attachment object
|
|
262
|
+
self._pastes = {} # chip_id -> full pasted text
|
|
263
|
+
self._next_file_id = 1
|
|
264
|
+
self._next_paste_id = 1
|
|
265
|
+
self.display = False
|
|
266
|
+
|
|
267
|
+
def _sync_visibility(self):
|
|
268
|
+
has_items = bool(self._attachments or self._pastes)
|
|
269
|
+
self.display = has_items
|
|
270
|
+
|
|
271
|
+
def on_mount(self):
|
|
272
|
+
self._sync_visibility()
|
|
273
|
+
|
|
274
|
+
def add_file(self, path, kind="file", source="browse"):
|
|
275
|
+
"""Validate → normalize → chip (requirements #14-16).
|
|
276
|
+
|
|
277
|
+
Returns the new chip_id, or None when the path was rejected.
|
|
278
|
+
Directories are rejected with an honest reason — only real
|
|
279
|
+
files attach. `source` records how the file arrived
|
|
280
|
+
("browse" | "drag_and_drop" | "paste" | "sidebar")."""
|
|
281
|
+
try:
|
|
282
|
+
exists = os.path.lexists(path)
|
|
283
|
+
is_dir = os.path.isdir(path)
|
|
284
|
+
except OSError:
|
|
285
|
+
return None
|
|
286
|
+
if not exists:
|
|
287
|
+
return None
|
|
288
|
+
if is_dir:
|
|
289
|
+
return None # directories never attach as files
|
|
290
|
+
chip_id = f"file-{self._next_file_id}"
|
|
291
|
+
self._next_file_id += 1
|
|
292
|
+
att = _core.Attachment(path)
|
|
293
|
+
att.source = source # Attachment uses __slots__; see below
|
|
294
|
+
if kind not in ("file",) and kind != att.kind:
|
|
295
|
+
att.kind = kind
|
|
296
|
+
att.extraction_status = _core.STATUS_READING
|
|
297
|
+
self._attachments[chip_id] = att
|
|
298
|
+
self._mount_file_chip(chip_id)
|
|
299
|
+
# active drop/attach feedback: flash the bar so a dropped or
|
|
300
|
+
# pasted batch visibly landed (never just an animation on an
|
|
301
|
+
# empty handler)
|
|
302
|
+
try:
|
|
303
|
+
self.add_class("cct-drop-active")
|
|
304
|
+
self.set_timer(0.6, lambda: self.remove_class("cct-drop-active"))
|
|
305
|
+
except Exception:
|
|
306
|
+
pass
|
|
307
|
+
self.post_message(AttachmentAdded(chip_id, att.name, kind=att.kind))
|
|
308
|
+
# Extraction off the UI thread; the chip flips to ready /
|
|
309
|
+
# failed when it lands (async worker, safe to await here).
|
|
310
|
+
self.run_worker(self._extract_worker(chip_id), group="attachments")
|
|
311
|
+
return chip_id
|
|
312
|
+
|
|
313
|
+
def get_attachment(self, chip_id):
|
|
314
|
+
return self._attachments.get(chip_id)
|
|
315
|
+
|
|
316
|
+
def _mount_file_chip(self, chip_id):
|
|
317
|
+
att = self._attachments.get(chip_id)
|
|
318
|
+
if att is None:
|
|
319
|
+
return
|
|
320
|
+
self._sync_visibility()
|
|
321
|
+
self.mount(Chip(chip_id, _chip_label(att),
|
|
322
|
+
on_delete=self._remove_file,
|
|
323
|
+
on_double_click=self._double_clicked))
|
|
324
|
+
return chip_id
|
|
325
|
+
|
|
326
|
+
def _double_clicked(self, chip_id):
|
|
327
|
+
att = self._attachments.get(chip_id)
|
|
328
|
+
if att is None:
|
|
329
|
+
return
|
|
330
|
+
self.post_message(AttachmentChipDoubleClicked(
|
|
331
|
+
chip_id, att.path, att.name))
|
|
332
|
+
|
|
333
|
+
def _refresh_chip(self, chip_id):
|
|
334
|
+
"""Re-render one chip's label from its Attachment's current
|
|
335
|
+
state — the status chips' reading -> ready/failed flip."""
|
|
336
|
+
for child in list(self.children):
|
|
337
|
+
if isinstance(child, Chip) and child.chip_id == chip_id:
|
|
338
|
+
att = self._attachments.get(chip_id)
|
|
339
|
+
if att is None:
|
|
340
|
+
return
|
|
341
|
+
child.update_label(_chip_label(att))
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
async def _extract_worker(self, chip_id):
|
|
345
|
+
import asyncio
|
|
346
|
+
att = self._attachments.get(chip_id)
|
|
347
|
+
if att is None or _core is None:
|
|
348
|
+
return
|
|
349
|
+
fresh = await asyncio.to_thread(_core.AttachmentManager.create, att.path)
|
|
350
|
+
att.kind = fresh.kind
|
|
351
|
+
att.metadata = fresh.metadata
|
|
352
|
+
att.content = fresh.content
|
|
353
|
+
att.extraction_status = fresh.extraction_status
|
|
354
|
+
att.error = fresh.error
|
|
355
|
+
if chip_id in self._attachments:
|
|
356
|
+
self._refresh_chip(chip_id)
|
|
357
|
+
|
|
358
|
+
def add_paste(self, text, line_count, word_count=0):
|
|
359
|
+
chip_id = f"paste-{self._next_paste_id}"
|
|
360
|
+
self._next_paste_id += 1
|
|
361
|
+
self._pastes[chip_id] = text
|
|
362
|
+
if line_count > 1:
|
|
363
|
+
label = f"[Pasted ~{line_count} lines]"
|
|
364
|
+
else:
|
|
365
|
+
label = f"[Pasted {len(text):,} characters]"
|
|
366
|
+
self._sync_visibility()
|
|
367
|
+
self.mount(Chip(chip_id, label, on_click=self._expand_paste,
|
|
368
|
+
on_delete=self._remove_paste))
|
|
369
|
+
self.post_message(PasteCollapsed(chip_id, line_count))
|
|
370
|
+
return chip_id
|
|
371
|
+
|
|
372
|
+
def pop_attachment_objects(self):
|
|
373
|
+
"""v0.7.8.1: returns every attached Attachment object (in
|
|
374
|
+
insertion order) and clears the bar's file chips (pastes are
|
|
375
|
+
left alone) — called right before a message is sent, so
|
|
376
|
+
attachments travel with that one message as real objects,
|
|
377
|
+
not bare paths. A chip still mid-extraction returns its
|
|
378
|
+
object anyway; the AI pipeline re-extracts if needed."""
|
|
379
|
+
out = list(self._attachments.values())
|
|
380
|
+
ids = set(self._attachments)
|
|
381
|
+
self._attachments = {}
|
|
382
|
+
for child in list(self.children):
|
|
383
|
+
if isinstance(child, Chip) and child.chip_id in ids:
|
|
384
|
+
child.remove()
|
|
385
|
+
self._sync_visibility()
|
|
386
|
+
return out
|
|
387
|
+
|
|
388
|
+
def pop_attachment_paths(self):
|
|
389
|
+
"""Backward-compatible shim — the pre-0.7.8.1 shape (paths).
|
|
390
|
+
New code should use pop_attachment_objects()."""
|
|
391
|
+
return [a.path for a in self.pop_attachment_objects()]
|
|
392
|
+
|
|
393
|
+
def _remove_file(self, chip_id):
|
|
394
|
+
if chip_id not in self._attachments:
|
|
395
|
+
return
|
|
396
|
+
self._attachments.pop(chip_id, None)
|
|
397
|
+
for child in list(self.children):
|
|
398
|
+
if isinstance(child, Chip) and child.chip_id == chip_id:
|
|
399
|
+
child.remove()
|
|
400
|
+
self._sync_visibility()
|
|
401
|
+
self.post_message(AttachmentRemoved(chip_id))
|
|
402
|
+
|
|
403
|
+
def _expand_paste(self, chip_id):
|
|
404
|
+
text = self._pastes.pop(chip_id, None)
|
|
405
|
+
if text is None:
|
|
406
|
+
return
|
|
407
|
+
for child in list(self.children):
|
|
408
|
+
if isinstance(child, Chip) and child.chip_id == chip_id:
|
|
409
|
+
child.remove()
|
|
410
|
+
self._sync_visibility()
|
|
411
|
+
self.post_message(PasteExpanded(chip_id, text))
|
|
412
|
+
|
|
413
|
+
def _remove_paste(self, chip_id):
|
|
414
|
+
if chip_id not in self._pastes:
|
|
415
|
+
return
|
|
416
|
+
self._pastes.pop(chip_id, None)
|
|
417
|
+
for child in list(self.children):
|
|
418
|
+
if isinstance(child, Chip) and child.chip_id == chip_id:
|
|
419
|
+
child.remove()
|
|
420
|
+
self._sync_visibility()
|
|
421
|
+
self.post_message(AttachmentRemoved(chip_id))
|
|
422
|
+
|
|
423
|
+
else:
|
|
424
|
+
AttachmentBar = None
|