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,286 @@
1
+ """
2
+ CCT AI Personalization (v0.7.8 BONUS 2).
3
+
4
+ A professional, honest personalization layer on top of the existing
5
+ prompt pipeline. The user keeps a set of named "profiles"; the active
6
+ profile adjusts every AI answer's style WITHOUT replacing any of CCT's
7
+ norman system prompts, tools, or memory:
8
+
9
+ - tone -> a short directive appended to the system prompt
10
+ (balanced / concise / detailed / playful / formal)
11
+ - additions -> free-form rules the user writes ("always use SI
12
+ units", "never mention the JSON protocol", ...)
13
+ - temperature -> a sampling override (0.0 - 2.0), applied where the
14
+ request config supports it — see request_config()
15
+ - model -> a preferred model label; advisory today (kept as
16
+ metadata the panel shows, because forcing a model
17
+ string across every provider/api_style the way a
18
+ real pin would is exactly the kind of deep rewrite
19
+ this passes on). It IS surfaced in the dashboard.
20
+ - description, top_p, creativity, reasoning, response_length,
21
+ memory_pref -> advisory metadata (v0.7.8.1 Personalize redesign):
22
+ stored on the profile and surfaced in the dashboard; top_p IS
23
+ forwarded to the request config (and payloads) where supported,
24
+ the rest describe the profile for future wiring and are honest
25
+ metadata, never fake settings.
26
+
27
+ Persistence: ~/.cct_profiles.json
28
+ {"active": "<name>", "profiles": [ {name, description, tone,
29
+ temperature, top_p, additions, model, creativity, reasoning,
30
+ response_length, memory_pref, created_at, updated_at}, ... ]}
31
+
32
+ Integration points (both single-line, no behavior rewrite):
33
+ - ai_modes.system_prompt_for — every non-agent UI mode prompt
34
+ - agent.run_agent — agent + ai personas (tools loop)
35
+
36
+ A profile never affects anything when none is active; callers that
37
+ want the raw prompt keep pinning their own system_prompt and nothing
38
+ changes.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import json
44
+ import os
45
+ import time
46
+
47
+ PROFILE_FILE = os.path.join(os.path.expanduser("~"), ".cct_profiles.json")
48
+
49
+ TONES = {
50
+ "balanced": "Keep a measured, even tone: clear and direct, neither terse nor padded.",
51
+ "concise": "Be concise. Answer in the fewest words that stay complete; prefer short sentences and no filler.",
52
+ "detailed": "Be thorough. Give full context, step-by-step reasoning, and complete derivations where relevant.",
53
+ "playful": "Keep a friendly, light tone. Stay accurate, but feel free to be a little playful and warm.",
54
+ "formal": "Be formal and precise. Use professional, carefully-phrased language without casualisms.",
55
+ }
56
+
57
+ DEFAULT_PROFILE = {
58
+ "name": "Balanced",
59
+ "description": "",
60
+ "tone": "balanced",
61
+ "temperature": None,
62
+ "top_p": None,
63
+ "additions": "",
64
+ "model": "",
65
+ "creativity": None,
66
+ "reasoning": "",
67
+ "response_length": "",
68
+ "memory_pref": "",
69
+ "created_at": 0,
70
+ "updated_at": 0,
71
+ }
72
+
73
+
74
+ def load_profiles() -> dict:
75
+ """Return {'active': name, 'profiles': [...]}. Never raises."""
76
+ try:
77
+ if os.path.exists(PROFILE_FILE):
78
+ with open(PROFILE_FILE, "r", encoding="utf-8") as f:
79
+ data = json.load(f)
80
+ if isinstance(data, dict):
81
+ profiles = data.get("profiles")
82
+ if isinstance(profiles, list):
83
+ cleaned = []
84
+ for p in profiles:
85
+ if not isinstance(p, dict):
86
+ continue
87
+ base = dict(DEFAULT_PROFILE)
88
+ base.update({k: p.get(k, v) for k, v in DEFAULT_PROFILE.items()})
89
+ cleaned.append(base)
90
+ return {"active": str(data.get("active") or ""), "profiles": cleaned}
91
+ except Exception:
92
+ pass
93
+ return {"active": "", "profiles": []}
94
+
95
+
96
+ def save_profiles(data: dict) -> bool:
97
+ try:
98
+ with open(PROFILE_FILE, "w", encoding="utf-8") as f:
99
+ json.dump({
100
+ "active": str(data.get("active") or ""),
101
+ "profiles": [dict(DEFAULT_PROFILE, **p) for p in (data.get("profiles") or [])],
102
+ }, f, indent=2)
103
+ return True
104
+ except Exception:
105
+ return False
106
+
107
+
108
+ def profiles() -> list[dict]:
109
+ return load_profiles().get("profiles", [])
110
+
111
+
112
+ def profile_names() -> list[str]:
113
+ return [p.get("name", "?") for p in profiles()]
114
+
115
+
116
+ def active_profile() -> dict | None:
117
+ """The active profile as a dict, or None when none is active or
118
+ the active name no longer exists."""
119
+ data = load_profiles()
120
+ name = data.get("active") or ""
121
+ if not name:
122
+ return None
123
+ for p in data.get("profiles", []):
124
+ if p.get("name") == name:
125
+ return p
126
+ return None
127
+
128
+
129
+ def set_active(name: str):
130
+ data = load_profiles()
131
+ data["active"] = name
132
+ save_profiles(data)
133
+
134
+
135
+ def add_profile(name: str, tone: str = "balanced", additions: str = "",
136
+ temperature=None, model: str = "", **extras) -> dict:
137
+ """Create or replace a profile by name (name is the key). Returns it.
138
+
139
+ `extras` are stored as-is (used by the Personalize panel for the
140
+ advisory fields: description, top_p, creativity, reasoning,
141
+ response_length, memory_pref)."""
142
+ now = int(time.time())
143
+ prof = {
144
+ "name": name,
145
+ "tone": tone if tone in TONES else "balanced",
146
+ "temperature": temperature,
147
+ "additions": additions,
148
+ "model": model,
149
+ "created_at": now,
150
+ "updated_at": now,
151
+ }
152
+ for key in ("description", "top_p", "creativity", "reasoning",
153
+ "response_length", "memory_pref"):
154
+ if extras.get(key) is not None:
155
+ prof[key] = extras[key]
156
+ data = load_profiles()
157
+ data["profiles"] = [p for p in data["profiles"] if p.get("name") != name]
158
+ data["profiles"].append(prof)
159
+ save_profiles(data)
160
+ return prof
161
+
162
+
163
+ def update_profile(name: str, **fields) -> dict | None:
164
+ prof = add_profile(name,
165
+ tone=fields.get("tone", "balanced"),
166
+ additions=fields.get("additions", ""),
167
+ temperature=fields.get("temperature"),
168
+ model=fields.get("model", ""),
169
+ description=fields.get("description"),
170
+ top_p=fields.get("top_p"),
171
+ creativity=fields.get("creativity"),
172
+ reasoning=fields.get("reasoning"),
173
+ response_length=fields.get("response_length"),
174
+ memory_pref=fields.get("memory_pref"))
175
+ return prof
176
+
177
+
178
+ def delete_profile(name: str):
179
+ data = load_profiles()
180
+ data["profiles"] = [p for p in data["profiles"] if p.get("name") != name]
181
+ if data.get("active") == name:
182
+ data["active"] = ""
183
+ save_profiles(data)
184
+
185
+
186
+ # ---------------------------------------------------------------------------
187
+ # Integration helpers
188
+
189
+ ABOUT_ME_FILE = os.path.join(os.path.expanduser("~"), ".cct_about_me.json")
190
+
191
+ def load_about_me() -> dict:
192
+ try:
193
+ if os.path.exists(ABOUT_ME_FILE):
194
+ with open(ABOUT_ME_FILE, "r", encoding="utf-8") as f:
195
+ data = json.load(f)
196
+ if isinstance(data, dict):
197
+ return data
198
+ except Exception:
199
+ pass
200
+ return {}
201
+
202
+
203
+ def about_me_block() -> str:
204
+ data = load_about_me()
205
+ if not data or not any(v.strip() for v in data.values() if isinstance(v, str)):
206
+ return ""
207
+ lines = []
208
+ for k, v in data.items():
209
+ if isinstance(v, str) and v.strip():
210
+ label = k.replace("_", " ").title()
211
+ lines.append(f"- {label}: {v.strip()}")
212
+ if not lines:
213
+ return ""
214
+ return "About the user:\n" + "\n".join(lines)
215
+
216
+
217
+ def is_memory_enabled() -> bool:
218
+ prof = active_profile()
219
+ if not prof:
220
+ return True
221
+ pref = (prof.get("memory_pref") or "auto").strip().lower()
222
+ if pref == "off":
223
+ return False
224
+ return True
225
+
226
+
227
+ def tone_directive(profile: dict | None) -> str:
228
+ if not profile:
229
+ return ""
230
+ tone = profile.get("tone") or "balanced"
231
+ base = TONES.get(tone, TONES["balanced"])
232
+ additions = (profile.get("additions") or "").strip()
233
+ if not additions:
234
+ return base
235
+ return base + "\nUser style rules (always follow these, they override defaults):\n" + additions
236
+
237
+
238
+ def personalize_system_prompt(base_prompt: str) -> str:
239
+ """Append the active profile's style directive + About Me to a system prompt."""
240
+ if os.environ.get("CCT_DISABLE_PERSONALIZATION"):
241
+ return base_prompt
242
+ prof = active_profile()
243
+ directive = tone_directive(prof)
244
+ about = about_me_block()
245
+ extra = ""
246
+ if directive:
247
+ extra += "\n\n<Personal style>\n" + directive + "\n</Personal style>"
248
+ if about:
249
+ extra += "\n\n<About Me>\n" + about + "\n</About Me>"
250
+ if not extra:
251
+ return base_prompt
252
+ if base_prompt.endswith(extra.strip()):
253
+ return base_prompt
254
+ return base_prompt.rstrip() + extra
255
+
256
+
257
+ def request_config() -> dict | None:
258
+ """Request-config overrides for the active profile: a dict with
259
+ 'temperature' and/or 'top_p' keys when the profile pins them, else
260
+ None. Pass as `config` to aicore.query_ai/stream_ai to merge over
261
+ the primary provider config (aicore merges {'temperature': ...} /
262
+ {'top_p': ...} into the OpenAI/Anthropic/Gemini payloads)."""
263
+ prof = active_profile()
264
+ if not prof:
265
+ return None
266
+ out = {}
267
+ temp = prof.get("temperature")
268
+ if temp is not None:
269
+ try:
270
+ out["temperature"] = float(temp)
271
+ except (TypeError, ValueError):
272
+ pass
273
+ top_p = prof.get("top_p")
274
+ if top_p is not None:
275
+ try:
276
+ out["top_p"] = float(top_p)
277
+ except (TypeError, ValueError):
278
+ pass
279
+ return out if out else None
280
+
281
+
282
+ def active_model_label() -> str:
283
+ prof = active_profile()
284
+ if not prof:
285
+ return ""
286
+ return prof.get("model") or ""
@@ -0,0 +1,213 @@
1
+ """
2
+ CCT — ai_preview_feedback.py: AI + Web Preview Feedback Loop (v0.7.10 spec #35).
3
+
4
+ Enables the AI to:
5
+ 1. See the current preview state (DOM snapshot, console errors)
6
+ 2. Take screenshots of the preview
7
+ 3. Interact with the preview (click, type, navigate)
8
+ 4. Get feedback on changes made
9
+
10
+ Architecture:
11
+ AI Agent -> FeedbackController -> PreviewController -> BrowserEngine
12
+ <- FeedbackController <- PreviewState
13
+ """
14
+
15
+ if __name__ == "__main__":
16
+ print("This is a library file and is not meant to be run directly.")
17
+ import sys
18
+ sys.exit(1)
19
+
20
+ import os
21
+ import threading
22
+ import time
23
+ from typing import Optional, Dict, Any, List
24
+ from dataclasses import dataclass, field
25
+
26
+
27
+ @dataclass
28
+ class PreviewFeedback:
29
+ """Feedback from the preview about the current state."""
30
+ url: str = ""
31
+ title: str = ""
32
+ dom_snapshot: str = ""
33
+ console_errors: List[str] = field(default_factory=list)
34
+ console_logs: List[str] = field(default_factory=list)
35
+ screenshot_path: str = ""
36
+ is_loading: bool = False
37
+ load_error: str = ""
38
+ timestamp: float = field(default_factory=time.time)
39
+
40
+
41
+ @dataclass
42
+ class AIAction:
43
+ """An action the AI wants to perform on the preview."""
44
+ action_type: str # "navigate", "click", "type", "screenshot", "evaluate"
45
+ target: str = "" # CSS selector, URL, or JS code
46
+ value: str = "" # Text to type, URL to navigate, etc.
47
+ callback: Optional[callable] = None
48
+
49
+
50
+ class AIPreviewFeedbackController:
51
+ """Controls the AI + Web Preview feedback loop.
52
+
53
+ This controller:
54
+ - Monitors preview state and provides feedback to the AI
55
+ - Executes AI actions on the preview
56
+ - Manages the feedback cycle
57
+ """
58
+
59
+ def __init__(self, preview_controller=None):
60
+ self.preview_controller = preview_controller
61
+ self._action_queue = []
62
+ self._feedback_history = []
63
+ self._lock = threading.Lock()
64
+ self._max_history = 50
65
+
66
+ def get_preview_state(self) -> PreviewFeedback:
67
+ """Get the current state of the preview for AI consumption."""
68
+ feedback = PreviewFeedback()
69
+
70
+ if not self.preview_controller:
71
+ feedback.load_error = "No preview controller available"
72
+ return feedback
73
+
74
+ try:
75
+ # Get URL and title
76
+ feedback.url = self.preview_controller.url or ""
77
+ if hasattr(self.preview_controller, 'engine'):
78
+ engine = self.preview_controller.engine
79
+ if hasattr(engine, 'current_title'):
80
+ feedback.title = engine.current_title or ""
81
+
82
+ # Get DOM snapshot
83
+ if hasattr(self.preview_controller, 'engine'):
84
+ engine = self.preview_controller.engine
85
+ if hasattr(engine, 'get_snapshot'):
86
+ snapshot = engine.get_snapshot()
87
+ if snapshot:
88
+ feedback.dom_snapshot = getattr(snapshot, 'text', '')
89
+
90
+ # Get console errors
91
+ if hasattr(self.preview_controller, 'engine'):
92
+ engine = self.preview_controller.engine
93
+ if hasattr(engine, 'console_errors'):
94
+ feedback.console_errors = list(engine.console_errors[-10:])
95
+
96
+ # Check loading state
97
+ if hasattr(self.preview_controller, 'preview_state'):
98
+ from .browser.state import PreviewState
99
+ feedback.is_loading = (
100
+ self.preview_controller.preview_state == PreviewState.STARTING
101
+ )
102
+
103
+ except Exception as e:
104
+ feedback.load_error = str(e)
105
+
106
+ return feedback
107
+
108
+ def execute_action(self, action: AIAction) -> bool:
109
+ """Execute an AI action on the preview."""
110
+ if not self.preview_controller:
111
+ return False
112
+
113
+ try:
114
+ if action.action_type == "navigate":
115
+ return self._navigate(action.target)
116
+ elif action.action_type == "click":
117
+ return self._click(action.target)
118
+ elif action.action_type == "type":
119
+ return self._type(action.target, action.value)
120
+ elif action.action_type == "screenshot":
121
+ return self._screenshot(action.target)
122
+ elif action.action_type == "evaluate":
123
+ return self._evaluate(action.target)
124
+ else:
125
+ return False
126
+ except Exception:
127
+ return False
128
+
129
+ def _navigate(self, url: str) -> bool:
130
+ """Navigate the preview to a URL."""
131
+ if not url.startswith("http"):
132
+ # Relative URL - make absolute
133
+ base = self.preview_controller.base_url or ""
134
+ url = base.rstrip("/") + "/" + url.lstrip("/")
135
+ self.preview_controller.navigate(url)
136
+ return True
137
+
138
+ def _click(self, selector: str) -> bool:
139
+ """Click an element in the preview."""
140
+ if hasattr(self.preview_controller, 'engine'):
141
+ engine = self.preview_controller.engine
142
+ if hasattr(engine, 'click'):
143
+ engine.click(selector)
144
+ return True
145
+ return False
146
+
147
+ def _type(self, selector: str, text: str) -> bool:
148
+ """Type text into an element in the preview."""
149
+ if hasattr(self.preview_controller, 'engine'):
150
+ engine = self.preview_controller.engine
151
+ if hasattr(engine, 'type'):
152
+ engine.type(selector, text)
153
+ return True
154
+ return False
155
+
156
+ def _screenshot(self, path: str) -> bool:
157
+ """Take a screenshot of the preview."""
158
+ if hasattr(self.preview_controller, 'engine'):
159
+ engine = self.preview_controller.engine
160
+ if hasattr(engine, 'screenshot'):
161
+ engine.screenshot(path)
162
+ return True
163
+ return False
164
+
165
+ def _evaluate(self, js_code: str) -> bool:
166
+ """Evaluate JavaScript in the preview."""
167
+ if hasattr(self.preview_controller, 'engine'):
168
+ engine = self.preview_controller.engine
169
+ if hasattr(engine, 'evaluate'):
170
+ engine.evaluate(js_code)
171
+ return True
172
+ return False
173
+
174
+ def record_feedback(self, feedback: PreviewFeedback):
175
+ """Record feedback for history."""
176
+ with self._lock:
177
+ self._feedback_history.append(feedback)
178
+ if len(self._feedback_history) > self._max_history:
179
+ self._feedback_history = self._feedback_history[-self._max_history:]
180
+
181
+ def get_feedback_summary(self) -> str:
182
+ """Get a summary of recent feedback for AI consumption."""
183
+ with self._lock:
184
+ if not self._feedback_history:
185
+ return "No preview feedback available."
186
+
187
+ recent = self._feedback_history[-5:] # Last 5 feedback items
188
+ summary = []
189
+ for fb in recent:
190
+ if fb.url:
191
+ summary.append(f"URL: {fb.url}")
192
+ if fb.title:
193
+ summary.append(f"Title: {fb.title}")
194
+ if fb.console_errors:
195
+ summary.append(f"Console errors: {len(fb.console_errors)}")
196
+ if fb.load_error:
197
+ summary.append(f"Error: {fb.load_error}")
198
+
199
+ return "\n".join(summary) if summary else "Preview state unknown."
200
+
201
+
202
+ # Global instance
203
+ _ai_feedback_controller = None
204
+
205
+
206
+ def get_ai_feedback_controller(preview_controller=None) -> AIPreviewFeedbackController:
207
+ """Get or create the global AI feedback controller."""
208
+ global _ai_feedback_controller
209
+ if _ai_feedback_controller is None:
210
+ _ai_feedback_controller = AIPreviewFeedbackController(preview_controller)
211
+ elif preview_controller and _ai_feedback_controller.preview_controller is None:
212
+ _ai_feedback_controller.preview_controller = preview_controller
213
+ return _ai_feedback_controller