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.
Files changed (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,209 @@
1
+ """
2
+ CCT — project_stats.py: pure functions computing the real, live
3
+ numbers the v0.7.2 roadmap's "Project & Workspace Dashboard" section
4
+ asks for. No Textual here — ui/dashboard.py renders what this returns.
5
+
6
+ Every function is capped/bounded and wrapped so a huge or unusual
7
+ workspace (a node_modules-sized folder, a symlink loop, a permission-
8
+ denied subdirectory) degrades to a partial/best-effort answer instead
9
+ of hanging the UI thread — these run synchronously on a call from
10
+ ui/dashboard.py's refresh, not in a worker, so a slow scan would
11
+ directly freeze the app.
12
+
13
+ Honest gaps, not fabricated: CPU% and a real "Workspace Health" score
14
+ need either `psutil` or continuous sampling this app doesn't do
15
+ anywhere else; rather than invent a number, cpu_percent() reports
16
+ `None` (not "0%") when it can't measure something real, and
17
+ callers show that as "unavailable" instead of a fake reading. Same
18
+ for "Indexed Status" — this codebase has no separate file index (the
19
+ Explorer's DirectoryTree reads the filesystem directly), so there's
20
+ nothing genuine to report; ui/dashboard.py says so rather than
21
+ displaying a made-up percentage.
22
+ """
23
+
24
+ if __name__ == "__main__":
25
+ print("This is a library file and is not meant to be run directly.")
26
+ import sys
27
+ sys.exit(1)
28
+
29
+ import os
30
+ import time
31
+
32
+ _IGNORE_DIRS = {".git", "node_modules", "__pycache__", ".venv", "venv",
33
+ ".mypy_cache", ".pytest_cache", "dist", "build", ".idea", ".vscode"}
34
+ _LANG_BY_EXT = {
35
+ ".py": "Python", ".js": "JavaScript", ".jsx": "JavaScript", ".ts": "TypeScript",
36
+ ".tsx": "TypeScript", ".java": "Java", ".c": "C", ".h": "C", ".cpp": "C++",
37
+ ".hpp": "C++", ".cs": "C#", ".go": "Go", ".rs": "Rust", ".rb": "Ruby",
38
+ ".php": "PHP", ".swift": "Swift", ".kt": "Kotlin", ".html": "HTML",
39
+ ".css": "CSS", ".scss": "SCSS", ".json": "JSON", ".yaml": "YAML",
40
+ ".yml": "YAML", ".md": "Markdown", ".sh": "Shell", ".sql": "SQL",
41
+ ".r": "R", ".lua": "Lua", ".dart": "Dart",
42
+ }
43
+ MAX_SCAN_FILES = 20000 # bail out past this so a huge workspace can't hang the UI thread
44
+
45
+
46
+ def scan_workspace(root):
47
+ """One bounded os.walk pass computing size/file-count/language
48
+ histogram/last-modified together, since walking the same tree three
49
+ separate times for three separate stats would be pure waste.
50
+ Returns a dict; every count is `None` if `root` isn't a real,
51
+ readable directory rather than a misleading 0."""
52
+ if not root or not os.path.isdir(root):
53
+ return {"total_size": None, "total_files": None, "languages": {},
54
+ "last_modified": None, "truncated": False}
55
+ total_size = 0
56
+ total_files = 0
57
+ languages = {}
58
+ last_modified = 0.0
59
+ truncated = False
60
+ for dirpath, dirnames, filenames in os.walk(root):
61
+ dirnames[:] = [d for d in dirnames if d not in _IGNORE_DIRS and not d.startswith(".")]
62
+ for name in filenames:
63
+ if total_files >= MAX_SCAN_FILES:
64
+ truncated = True
65
+ break
66
+ path = os.path.join(dirpath, name)
67
+ try:
68
+ st = os.stat(path)
69
+ except OSError:
70
+ continue
71
+ total_size += st.st_size
72
+ total_files += 1
73
+ last_modified = max(last_modified, st.st_mtime)
74
+ ext = os.path.splitext(name)[1].lower()
75
+ lang = _LANG_BY_EXT.get(ext)
76
+ if lang:
77
+ languages[lang] = languages.get(lang, 0) + 1
78
+ if truncated:
79
+ break
80
+ return {
81
+ "total_size": total_size, "total_files": total_files,
82
+ "languages": dict(sorted(languages.items(), key=lambda kv: -kv[1])[:5]),
83
+ "last_modified": last_modified or None, "truncated": truncated,
84
+ }
85
+
86
+
87
+ def human_size(n):
88
+ if n is None:
89
+ return "\u2014"
90
+ for unit in ("B", "KB", "MB", "GB", "TB"):
91
+ if n < 1024 or unit == "TB":
92
+ return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}"
93
+ n /= 1024
94
+ return f"{n:.1f} TB"
95
+
96
+
97
+ def human_age(epoch):
98
+ if not epoch:
99
+ return "\u2014"
100
+ delta = time.time() - epoch
101
+ if delta < 60:
102
+ return "just now"
103
+ if delta < 3600:
104
+ return f"{int(delta // 60)}m ago"
105
+ if delta < 86400:
106
+ return f"{int(delta // 3600)}h ago"
107
+ return f"{int(delta // 86400)}d ago"
108
+
109
+
110
+ def git_branch(root):
111
+ """Reads .git/HEAD directly rather than shelling out to `git` — no
112
+ subprocess dependency, and it's the one file that answers "what
113
+ branch" without needing a full git status walk. Returns None if
114
+ `root` isn't a git repo at all (distinct from "detached HEAD",
115
+ which returns the short commit hash instead of a branch name)."""
116
+ head_path = os.path.join(root or "", ".git", "HEAD")
117
+ try:
118
+ with open(head_path, "r", encoding="utf-8") as f:
119
+ content = f.read().strip()
120
+ except OSError:
121
+ return None
122
+ if content.startswith("ref:"):
123
+ return content.split("/")[-1]
124
+ return content[:8] # detached HEAD: short commit hash
125
+
126
+
127
+ _DEP_FILES = ("requirements.txt", "package.json", "pyproject.toml", "Pipfile")
128
+
129
+
130
+ def dependencies(root):
131
+ """Best-effort dependency list from whichever manifest exists —
132
+ real names read from the actual file, not a fabricated guess.
133
+ package.json is parsed as JSON; the others are read as plain lines/
134
+ TOML-ish text with a conservative regex rather than pulling in a
135
+ TOML parser dependency for one dashboard field. Capped at 12 names
136
+ total so a monorepo's 300-package lockfile doesn't blow out the
137
+ dashboard layout."""
138
+ if not root:
139
+ return []
140
+ names = []
141
+ req = os.path.join(root, "requirements.txt")
142
+ if os.path.isfile(req):
143
+ try:
144
+ with open(req, "r", encoding="utf-8", errors="ignore") as f:
145
+ for line in f:
146
+ line = line.strip()
147
+ if line and not line.startswith("#"):
148
+ import re
149
+ m = re.match(r"^([A-Za-z0-9_.\-]+)", line)
150
+ if m:
151
+ names.append(m.group(1))
152
+ except OSError:
153
+ pass
154
+ pkg = os.path.join(root, "package.json")
155
+ if os.path.isfile(pkg):
156
+ try:
157
+ import json
158
+ with open(pkg, "r", encoding="utf-8") as f:
159
+ data = json.load(f)
160
+ names.extend(list(data.get("dependencies", {}).keys()))
161
+ names.extend(list(data.get("devDependencies", {}).keys()))
162
+ except Exception:
163
+ pass
164
+ pyproject = os.path.join(root, "pyproject.toml")
165
+ if os.path.isfile(pyproject):
166
+ try:
167
+ import re
168
+ with open(pyproject, "r", encoding="utf-8", errors="ignore") as f:
169
+ text = f.read()
170
+ for m in re.finditer(r'^\s*"?([A-Za-z0-9_.\-]+)"?\s*=', text, re.MULTILINE):
171
+ if m.group(1).lower() not in ("name", "version", "description",
172
+ "authors", "readme", "requires-python"):
173
+ names.append(m.group(1))
174
+ except OSError:
175
+ pass
176
+ seen, out = set(), []
177
+ for n in names:
178
+ if n.lower() not in seen:
179
+ seen.add(n.lower())
180
+ out.append(n)
181
+ if len(out) >= 12:
182
+ break
183
+ return out
184
+
185
+
186
+ def memory_usage_mb():
187
+ """Real resident-memory reading for THIS process, via stdlib
188
+ `resource` on POSIX (no psutil dependency needed for this one
189
+ number). Returns None on platforms where `resource` doesn't exist
190
+ (Windows) rather than faking a number — see module docstring."""
191
+ try:
192
+ import resource
193
+ rss_kb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
194
+ # macOS reports bytes, Linux reports KB — ru_maxrss's unit is
195
+ # platform-specific by design in the POSIX API itself.
196
+ import sys as _sys
197
+ if _sys.platform == "darwin":
198
+ return rss_kb / (1024 * 1024)
199
+ return rss_kb / 1024
200
+ except Exception:
201
+ return None
202
+
203
+
204
+ def cpu_percent():
205
+ """No portable, dependency-free way to sample process CPU% without
206
+ either `psutil` or maintaining our own two-sample delta over time
207
+ (which nothing in this app currently does) — returns None rather
208
+ than a fabricated number. See module docstring."""
209
+ return None
@@ -0,0 +1,328 @@
1
+ """
2
+ CCT Recent/Pinned/Favorite Projects (spec v0.7 Sidebar section +
3
+ v0.7.7 Workspace Management Preview, sections 1/4/5).
4
+
5
+ A small, dedicated JSON store — same pattern as workspace.py and
6
+ config.py rather than growing either of those. Tracks folders opened
7
+ via the IDE's "Open Folder" (ui/sidebar.py), so they can be reopened
8
+ "without restarting" (spec's own phrase) and shown in the Welcome
9
+ Dashboard's "Recent Projects" list.
10
+
11
+ v0.7.7 additions (all backward-compatible — every pre-existing
12
+ function keeps its exact old behavior):
13
+ - per-entry labels (rename), favorites, and per-workspace sidebar
14
+ collapse state;
15
+ - `workspaces()` — one combined list of every known workspace with
16
+ all metadata, for the multi-workspace explorer and the history
17
+ panel;
18
+ - `search()`, `remove()`, `clear_history()`, `last_opened()`.
19
+
20
+ Deliberately NOT part of config.py's CCTConfig dataclass: that file's
21
+ own docstring already says new settings should land there going
22
+ forward, but a growing, unbounded list of recent paths is list-shaped
23
+ state, not a scalar setting — mixing the two would make CCTConfig's
24
+ schema noisy for the (many) callers that only care about the scalar
25
+ settings.
26
+ """
27
+
28
+ if __name__ == "__main__":
29
+ print("This is a library file and is not meant to be run directly.")
30
+ import sys
31
+ sys.exit(1)
32
+
33
+ import json
34
+ import os
35
+ import time
36
+
37
+ STORE_PATH = os.path.join(os.path.expanduser("~"), ".cct_recent_projects.json")
38
+ # v0.7.8.1: Recent Workspaces is now the primary workspace-management
39
+ # surface, so the history cap grows from 12 to 20.
40
+ MAX_RECENT = 20
41
+
42
+
43
+ def _load():
44
+ if not os.path.exists(STORE_PATH):
45
+ return {"recent": [], "pinned": []}
46
+ try:
47
+ with open(STORE_PATH, "r", encoding="utf-8") as f:
48
+ data = json.load(f)
49
+ data.setdefault("recent", [])
50
+ data.setdefault("pinned", [])
51
+ return data
52
+ except Exception:
53
+ return {"recent": [], "pinned": []}
54
+
55
+
56
+ def _save(data):
57
+ try:
58
+ with open(STORE_PATH, "w", encoding="utf-8") as f:
59
+ json.dump(data, f, indent=2)
60
+ return True
61
+ except Exception:
62
+ return False
63
+
64
+
65
+ def _norm(path):
66
+ return os.path.abspath(os.path.expanduser(str(path)))
67
+
68
+
69
+ def _get_entry(data, path):
70
+ for r in data["recent"]:
71
+ if r.get("path") == path:
72
+ return r
73
+ return None
74
+
75
+
76
+ def record_opened(path):
77
+ """Call whenever a folder is opened as the IDE workspace root.
78
+ Moves `path` to the front of 'recent', de-duplicated, capped at
79
+ MAX_RECENT so this file doesn't grow forever. Existing metadata
80
+ (label/favorite/collapsed) on the entry is preserved across
81
+ reopens."""
82
+ path = _norm(path)
83
+ data = _load()
84
+ old = _get_entry(data, path)
85
+ meta = {}
86
+ if old is not None:
87
+ meta = {k: old[k] for k in ("label", "favorite", "collapsed") if old.get(k) is not None}
88
+ data["recent"] = [r for r in data["recent"] if r.get("path") != path]
89
+ entry = {"path": path, "opened_at": time.time()}
90
+ entry.update(meta)
91
+ data["recent"].insert(0, entry)
92
+ data["recent"] = data["recent"][:MAX_RECENT]
93
+ _save(data)
94
+ return data
95
+
96
+
97
+ def toggle_pin(path):
98
+ path = _norm(path)
99
+ data = _load()
100
+ pinned = data["pinned"]
101
+ if path in pinned:
102
+ pinned.remove(path)
103
+ is_pinned = False
104
+ else:
105
+ pinned.insert(0, path)
106
+ is_pinned = True
107
+ data["pinned"] = pinned
108
+ _save(data)
109
+ return is_pinned
110
+
111
+
112
+ def is_pinned(path):
113
+ path = _norm(path)
114
+ return path in _load()["pinned"]
115
+
116
+
117
+ def remove_recent(path):
118
+ """Removes a single entry from 'recent' (matched by absolute path) —
119
+ the per-project delete option in the Explorer's Recent Projects
120
+ list. Never touches 'pinned' (unpinning is a separate, deliberate
121
+ action), and never touches any file or folder on disk. Returns
122
+ True/False the same way _save() does; safe to call for a path
123
+ that isn't in the list (no-op)."""
124
+ path = _norm(path)
125
+ data = _load()
126
+ data["recent"] = [r for r in data["recent"] if r.get("path") != path]
127
+ return _save(data)
128
+
129
+
130
+ def clear_recent():
131
+ """Removes every entry from 'recent' (only the stored references in
132
+ STORE_PATH) — never touches 'pinned', and never touches any actual
133
+ file or folder on disk. Safe to call even with an empty list
134
+ already; returns True/False the same way _save() does."""
135
+ data = _load()
136
+ data["recent"] = []
137
+ return _save(data)
138
+
139
+
140
+ def forget_protected():
141
+ """v0.7.9.4 fix: purge any recent/pinned entry that lives inside a
142
+ protected system root (C:\\Windows, C:\\Program Files, ...). An
143
+ earlier build could record e.g. C:\\WINDOWS\\System32 as a 'recent
144
+ project' when `cat` was launched from PowerShell's default cwd;
145
+ reopening such a directory is never useful (generated-file creation
146
+ fails with PermissionError there). Runs once at UI startup. Never
147
+ raises."""
148
+ try:
149
+ from . import workspace as _ws
150
+ data = _load()
151
+ before = (len(data["recent"]), len(data["pinned"]))
152
+ data["recent"] = [r for r in data["recent"]
153
+ if not _ws.path_is_protected(r.get("path", ""))]
154
+ data["pinned"] = [p for p in data["pinned"]
155
+ if not _ws.path_is_protected(
156
+ p.get("path", "") if isinstance(p, dict)
157
+ else str(p))]
158
+ if (len(data["recent"]), len(data["pinned"])) != before:
159
+ _save(data)
160
+ except Exception:
161
+ pass
162
+
163
+
164
+ def recent():
165
+ return [r["path"] for r in _load()["recent"]]
166
+
167
+
168
+ def pinned():
169
+ return list(_load()["pinned"])
170
+
171
+
172
+ # ================================================== v0.7.7 workspace mgmt ==
173
+ # (Workspace Management Preview, sections 4/5 — everything below is
174
+ # additive; nothing above changed behavior.)
175
+
176
+
177
+ def label_for(path):
178
+ """The workspace's display label: a user-set custom label, else the
179
+ folder's base name."""
180
+ path = _norm(path)
181
+ entry = _get_entry(_load(), path)
182
+ if entry and entry.get("label"):
183
+ return entry["label"]
184
+ return os.path.basename(path.rstrip(os.sep)) or path
185
+
186
+
187
+ def rename(path, label):
188
+ """Sets a custom display label for a workspace. Empty/None clears
189
+ it back to the folder-name default. Returns the effective label."""
190
+ path = _norm(path)
191
+ label = (label or "").strip()
192
+ data = _load()
193
+ entry = _get_entry(data, path)
194
+ if entry is None:
195
+ data["recent"].insert(0, {"path": path, "opened_at": time.time(),
196
+ "label": label or None})
197
+ else:
198
+ if label:
199
+ entry["label"] = label
200
+ else:
201
+ entry.pop("label", None)
202
+ _save(data)
203
+ return label or os.path.basename(path.rstrip(os.sep)) or path
204
+
205
+
206
+ def toggle_favorite(path):
207
+ """Stars/unstars a workspace. Returns the new favorite state."""
208
+ path = _norm(path)
209
+ data = _load()
210
+ entry = _get_entry(data, path)
211
+ if entry is None:
212
+ data["recent"].insert(0, {"path": path, "opened_at": time.time(),
213
+ "favorite": True})
214
+ _save(data)
215
+ return True
216
+ favorite = not bool(entry.get("favorite"))
217
+ entry["favorite"] = favorite
218
+ _save(data)
219
+ return favorite
220
+
221
+
222
+ def is_favorite(path):
223
+ path = _norm(path)
224
+ entry = _get_entry(_load(), path)
225
+ return bool(entry and entry.get("favorite"))
226
+
227
+
228
+ def favorites():
229
+ data = _load()
230
+ return [r["path"] for r in data["recent"] if r.get("favorite")]
231
+
232
+
233
+ def set_collapsed(path, collapsed):
234
+ """Stores the explorer collapse state for one workspace (the
235
+ multi-workspace explorer collapses a workspace's tree without
236
+ removing it)."""
237
+ path = _norm(path)
238
+ data = _load()
239
+ entry = _get_entry(data, path)
240
+ if entry is None:
241
+ data["recent"].insert(0, {"path": path, "opened_at": time.time(),
242
+ "collapsed": bool(collapsed)})
243
+ else:
244
+ entry["collapsed"] = bool(collapsed)
245
+ _save(data)
246
+
247
+
248
+ def is_collapsed(path):
249
+ path = _norm(path)
250
+ entry = _get_entry(_load(), path)
251
+ return bool(entry and entry.get("collapsed"))
252
+
253
+
254
+ def workspaces():
255
+ """One combined list of every known workspace — recent + pinned
256
+ union, each entry {"path", "label", "pinned", "favorite",
257
+ "collapsed", "opened_at"}. Pinned entries missing from recent are
258
+ included with their base-name label. Ordered: pinned first, then
259
+ most-recently-opened."""
260
+ data = _load()
261
+ by_path = {}
262
+ for r in data["recent"]:
263
+ by_path[r["path"]] = {
264
+ "path": r["path"],
265
+ "label": r.get("label") or os.path.basename(r["path"].rstrip(os.sep)) or r["path"],
266
+ "favorite": bool(r.get("favorite")),
267
+ "collapsed": bool(r.get("collapsed")),
268
+ "opened_at": r.get("opened_at", 0),
269
+ "pinned": False,
270
+ }
271
+ for p in data["pinned"]:
272
+ p = _norm(p)
273
+ if p in by_path:
274
+ by_path[p]["pinned"] = True
275
+ else:
276
+ by_path[p] = {
277
+ "path": p,
278
+ "label": os.path.basename(p.rstrip(os.sep)) or p,
279
+ "favorite": False,
280
+ "collapsed": False,
281
+ "opened_at": 0,
282
+ "pinned": True,
283
+ }
284
+ ordered = sorted(by_path.values(),
285
+ key=lambda w: (not w["pinned"], -w.get("opened_at", 0)))
286
+ return ordered
287
+
288
+
289
+ def last_opened():
290
+ """The most recently opened workspace path, or None."""
291
+ r = _load()["recent"]
292
+ return r[0]["path"] if r else None
293
+
294
+
295
+ def search(query):
296
+ """Case-insensitive search across every known workspace's path and
297
+ label. Empty query returns everything (workspaces())."""
298
+ q = (query or "").strip().lower()
299
+ if not q:
300
+ return workspaces()
301
+ return [w for w in workspaces()
302
+ if q in w["path"].lower() or q in w["label"].lower()]
303
+
304
+
305
+ def remove(path):
306
+ """Full removal from the history: recent, pinned, and favorite
307
+ references (pinned list holds bare paths). Never touches files on
308
+ disk. Returns True if anything was removed."""
309
+ path = _norm(path)
310
+ data = _load()
311
+ before = (len(data["recent"]), len(data["pinned"]))
312
+ data["recent"] = [r for r in data["recent"] if r.get("path") != path]
313
+ data["pinned"] = [p for p in data["pinned"] if p != path]
314
+ changed = (len(data["recent"]), len(data["pinned"])) != before
315
+ _save(data)
316
+ return changed
317
+
318
+
319
+ def clear_history():
320
+ """Removes every stored workspace reference (recent + pinned +
321
+ favorites) — only the JSON store, never files on disk. Returns
322
+ True when the store had anything to clear."""
323
+ data = _load()
324
+ had = bool(data["recent"] or data["pinned"])
325
+ data["recent"] = []
326
+ data["pinned"] = []
327
+ _save(data)
328
+ return had
File without changes
@@ -0,0 +1,80 @@
1
+ """
2
+ calc_terminal/providers/adapters/__init__.py
3
+ ===========================================
4
+ Provider Adapter Registry and Factory for CAT.
5
+ """
6
+
7
+ from typing import Dict, Type, Optional, Any
8
+
9
+ from .base import BaseProviderAdapter
10
+ from .openai_adapter import OpenAIAdapter
11
+ from .anthropic_adapter import AnthropicAdapter
12
+ from .gemini_adapter import GeminiAdapter
13
+ from .chinese_adapters import (
14
+ DeepSeekAdapter,
15
+ QwenAdapter,
16
+ MoonshotAdapter,
17
+ ZhipuGLMAdapter,
18
+ MiniMaxAdapter,
19
+ TencentHunyuanAdapter,
20
+ ByteDanceSeedAdapter,
21
+ XiaomiMiMoAdapter,
22
+ BaiduErnieAdapter,
23
+ )
24
+ from .ollama_adapter import OllamaAdapter
25
+ from .other_adapters import (
26
+ XAIAdapter,
27
+ MistralAdapter,
28
+ CohereAdapter,
29
+ GenericOpenAIAdapter,
30
+ )
31
+
32
+ _ADAPTER_REGISTRY: Dict[str, Type[BaseProviderAdapter]] = {
33
+ "openai": OpenAIAdapter,
34
+ "anthropic": AnthropicAdapter,
35
+ "gemini": GeminiAdapter,
36
+ "google": GeminiAdapter,
37
+ "deepseek": DeepSeekAdapter,
38
+ "alibaba": QwenAdapter,
39
+ "qwen": QwenAdapter,
40
+ "moonshot": MoonshotAdapter,
41
+ "kimi": MoonshotAdapter,
42
+ "zhipu": ZhipuGLMAdapter,
43
+ "glm": ZhipuGLMAdapter,
44
+ "minimax": MiniMaxAdapter,
45
+ "tencent": TencentHunyuanAdapter,
46
+ "hunyuan": TencentHunyuanAdapter,
47
+ "bytedance": ByteDanceSeedAdapter,
48
+ "seed": ByteDanceSeedAdapter,
49
+ "doubao": ByteDanceSeedAdapter,
50
+ "xiaomi": XiaomiMiMoAdapter,
51
+ "mimo": XiaomiMiMoAdapter,
52
+ "baidu": BaiduErnieAdapter,
53
+ "ernie": BaiduErnieAdapter,
54
+ "ollama": OllamaAdapter,
55
+ "xai": XAIAdapter,
56
+ "grok": XAIAdapter,
57
+ "mistral": MistralAdapter,
58
+ "cohere": CohereAdapter,
59
+ }
60
+
61
+
62
+ def register_adapter(provider_id: str, adapter_cls: Type[BaseProviderAdapter]):
63
+ """Allow registering external or custom provider adapters."""
64
+ _ADAPTER_REGISTRY[provider_id.lower().strip()] = adapter_cls
65
+
66
+
67
+ def get_adapter(provider_id: str, config: Optional[Dict[str, Any]] = None) -> BaseProviderAdapter:
68
+ """Instantiate the appropriate adapter for the given provider."""
69
+ pid = (provider_id or "").lower().strip()
70
+ cls = _ADAPTER_REGISTRY.get(pid)
71
+ if cls is None:
72
+ adapter = GenericOpenAIAdapter(config)
73
+ adapter.provider_id = pid
74
+ adapter.display_name = pid.capitalize()
75
+ return adapter
76
+ return cls(config)
77
+
78
+
79
+ def list_supported_adapters() -> Dict[str, Type[BaseProviderAdapter]]:
80
+ return dict(_ADAPTER_REGISTRY)