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,752 @@
1
+ """
2
+ CCT AI Modes (spec "v0.6.2 Quantum Edition", section 3).
3
+
4
+ Four themed personas for the SAME underlying AI pipeline
5
+ (aicore.stream_ai / agent.run_agent) — switching modes never swaps
6
+ providers, models, or the network layer. What it changes:
7
+
8
+ - which system prompt is sent (this module owns that routing)
9
+ - which accent color the primary UI paints itself with (mark,
10
+ borders, badges, breadcrumb — see ui/theme_css.py's
11
+ `css_variables()`, which reads `current_mode()`/`accent_hex()`
12
+ from here on every repaint)
13
+
14
+ This supersedes ComposerFooter's old NOTEBOOK/AGENT binary toggle
15
+ (the `_toggle_notebook` badge in ui/footer.py) by growing it from two
16
+ stops to four. The event it posts (events.NotebookChanged) is
17
+ unchanged on purpose — only the set of valid `.mode` values grows —
18
+ so nothing downstream of that event needed a rename to keep working.
19
+
20
+ Real capability differences, not just color:
21
+ - Notebook — plain aicore.stream_ai with the existing default chat
22
+ prompt (unchanged behavior, still the app's baseline).
23
+ - Agent — routes through agent.run_agent(mode="agent"), which is
24
+ CCT's actual tool-executing loop (solve/plot/simulate for real,
25
+ not just describe). This is the one mode that can't stream
26
+ token-by-token today because run_agent() is a blocking call that
27
+ returns a finished answer — see ui/app.py's `_stream_worker` for
28
+ how that's surfaced honestly (one chunk, not simulated typing).
29
+ - Build — plain aicore.stream_ai with a software-development system
30
+ prompt (code generation/debugging/architecture), no CCT chemistry
31
+ tool loop — building software isn't a chemistry-tool task.
32
+ - Plan — plain aicore.stream_ai with a planning/roadmap system
33
+ prompt, same reasoning as Build.
34
+ - Research (v0.7.7) — plain aicore.stream_ai with a research system
35
+ prompt; the agent's web_search / deep_research tools are always
36
+ reachable from any mode with a workspace open (same gate as Build).
37
+ - Debugger (v0.7.7) — plain aicore.stream_ai with an error-analysis /
38
+ performance / security analysis system prompt.
39
+ """
40
+
41
+ if __name__ == "__main__":
42
+ print("This is a library file and is not meant to be run directly.")
43
+ import sys
44
+ sys.exit(1)
45
+
46
+ from . import identity
47
+
48
+ MODE_ORDER = ["notebook", "research", "plan", "build", "debugger", "agent"]
49
+
50
+ # key -> (label, icon, accent RGB, one-line purpose). Accent colors are
51
+ # the spec's literal choices (Notebook blue / Planner green / Research
52
+ # purple / Build orange / Debugger red / Agent violet), picked from the
53
+ # same Tokyo-Night-adjacent family theme.py/theme_css.py already use
54
+ # elsewhere so a mode switch looks like a deliberate accent change, not
55
+ # an off-palette clash.
56
+ # v0.7.8 (FEATURE 3 — Mode Gradients): every mode also carries a two-
57
+ # stop `gradient` (start, end) — never a flat color. The gradient is
58
+ # exposed through theme_css.css_variables() as $accent-gradient-start /
59
+ # $accent-gradient-end and used by the composer badge, welcome screen
60
+ # and header mark, with the primary accent kept as the solid fallback
61
+ # for borders/bubbles.
62
+ MODE_META = {
63
+ "notebook": dict(
64
+ label="Notebook", icon="\U0001f4d8", accent=(122, 162, 247), # blue
65
+ purpose="Daily AI \u2014 chemistry, physics, mathematics, explanations, research.",
66
+ gradient=((93, 132, 240), (145, 190, 255)), # blue gradient
67
+ ),
68
+ "research": dict(
69
+ label="Research", icon="\U0001f50d", accent=(255, 105, 180), # hot pink
70
+ purpose="Deep analysis \u2014 documentation search, API research, comparisons, web research.",
71
+ gradient=((255, 192, 203), (255, 105, 180)), # pink to hot pink
72
+ ),
73
+ "plan": dict(
74
+ label="Plan", icon="\U0001f9ed", accent=(158, 206, 106), # green
75
+ purpose="Project planning \u2014 roadmaps, brainstorming, concepts, prototypes, research/study plans.",
76
+ gradient=((120, 180, 80), (185, 230, 140)), # green gradient
77
+ ),
78
+ "build": dict(
79
+ label="Build", icon="\U0001f528", accent=(224, 175, 104), # orange
80
+ purpose="Software development \u2014 code generation, debugging, architecture, deployment planning.",
81
+ gradient=((230, 150, 70), (250, 200, 130)), # orange gradient
82
+ ),
83
+ "debugger": dict(
84
+ label="Debugger", icon="\U0001f41b", accent=(220, 20, 60), # red
85
+ purpose="Diagnosis \u2014 error analysis, crash investigation, profiling, security analysis.",
86
+ gradient=((220, 20, 60), (255, 150, 150)), # red to light red
87
+ ),
88
+ "agent": dict(
89
+ label=" Agent", icon="\u2699", accent=(187, 154, 247), # purple
90
+ purpose="Autonomous execution \u2014 multi-step workflows, tool usage, simulations, file operations.",
91
+ gradient=((150, 120, 240), (210, 180, 255)), # violet gradient
92
+ ),
93
+ }
94
+
95
+ # --- defaults snapshot for reset (STRICT v0.7.9.7) ---
96
+ import copy as _copy
97
+ _DEFAULT_MODE_META = _copy.deepcopy(MODE_META)
98
+
99
+ def _hex_to_rgb(hex_str):
100
+ """'#rrggbb' or 'rrggbb' -> (r,g,b) or None if invalid."""
101
+ if not isinstance(hex_str, str):
102
+ return None
103
+ s = hex_str.strip().lstrip("#")
104
+ if len(s) != 6:
105
+ return None
106
+ try:
107
+ r = int(s[0:2], 16); g = int(s[2:4], 16); b = int(s[4:6], 16)
108
+ return (r, g, b)
109
+ except ValueError:
110
+ return None
111
+
112
+ def _rgb_to_hex(rgb):
113
+ r, g, b = rgb
114
+ return f"#{r:02x}{g:02x}{b:02x}"
115
+
116
+ def _blend(c1, c2, t):
117
+ return tuple(int(round(a + (b - a) * t)) for a, b in zip(c1, c2))
118
+
119
+ def _auto_gradient(accent_rgb):
120
+ """Auto-generate a two-stop gradient from a single accent color.
121
+ Darker start (blend toward black 18%) and lighter end (blend toward white 26%) —
122
+ produces the same subtle depth as the hand-authored gradients without
123
+ requiring the user to pick two colors. Validated to keep start != end
124
+ and both within 0-255."""
125
+ darker = _blend(accent_rgb, (0, 0, 0), 0.18)
126
+ lighter = _blend(accent_rgb, (255, 255, 255), 0.26)
127
+ # ensure not identical to accent (happens on very dark/light extremes)
128
+ if darker == lighter:
129
+ lighter = _blend(accent_rgb, (255, 255, 255), 0.35)
130
+ return (darker, lighter)
131
+
132
+ def _apply_custom_colors():
133
+ """Load user custom colors from central config (~/.cct_config.json)
134
+ and patch MODE_META in-memory. Called once at import and after any
135
+ set/reset operation. Never raises."""
136
+ try:
137
+ from . import config as _cfg
138
+ cfg = _cfg.load_config()
139
+ custom = getattr(cfg, "accent_colors", None) or {}
140
+ if not isinstance(custom, dict):
141
+ return
142
+ for mode_key, hex_val in custom.items():
143
+ if mode_key not in MODE_META:
144
+ continue
145
+ rgb = _hex_to_rgb(hex_val)
146
+ if rgb is None:
147
+ continue
148
+ MODE_META[mode_key]["accent"] = rgb
149
+ # auto gradient unless user stored explicit gradient (future)
150
+ # check if config has accent_gradients for this mode
151
+ grad = None
152
+ try:
153
+ grads = getattr(cfg, "accent_gradients", None) or {}
154
+ if isinstance(grads, dict) and mode_key in grads:
155
+ gv = grads[mode_key]
156
+ # gv expected as [start_hex, end_hex] or tuple
157
+ if isinstance(gv, (list, tuple)) and len(gv) == 2:
158
+ s_rgb = _hex_to_rgb(gv[0]); e_rgb = _hex_to_rgb(gv[1])
159
+ if s_rgb and e_rgb:
160
+ grad = (s_rgb, e_rgb)
161
+ except Exception:
162
+ grad = None
163
+ if grad is None:
164
+ grad = _auto_gradient(rgb)
165
+ MODE_META[mode_key]["gradient"] = grad
166
+ except Exception:
167
+ pass
168
+
169
+ def _persist_custom_colors():
170
+ """Persist current MODE_META accents back to config.accent_colors.
171
+ Best-effort; never raises."""
172
+ try:
173
+ from . import config as _cfg
174
+ cfg = _cfg.load_config()
175
+ out = {}
176
+ for k in MODE_META:
177
+ out[k] = _rgb_to_hex(MODE_META[k]["accent"])
178
+ cfg.accent_colors = out
179
+ # also persist gradients if needed for explicit storage
180
+ # keep auto-generated gradients in sync but don't require user to store them;
181
+ # we store them so a future manual gradient edit survives
182
+ if not hasattr(cfg, "accent_gradients"):
183
+ # add attribute dynamically if missing (backward compat)
184
+ try:
185
+ setattr(cfg, "accent_gradients", {})
186
+ except Exception:
187
+ pass
188
+ try:
189
+ grads = {}
190
+ for k in MODE_META:
191
+ g = MODE_META[k].get("gradient")
192
+ if g and len(g) == 2:
193
+ grads[k] = [_rgb_to_hex(g[0]), _rgb_to_hex(g[1])]
194
+ if hasattr(cfg, "accent_gradients"):
195
+ cfg.accent_gradients = grads
196
+ except Exception:
197
+ pass
198
+ _cfg.save_config(cfg)
199
+ except Exception:
200
+ pass
201
+
202
+ def get_mode_colors():
203
+ """Return {mode_key: '#rrggbb'} snapshot of current accents."""
204
+ return {k: _rgb_to_hex(v["accent"]) for k, v in MODE_META.items()}
205
+
206
+ def get_mode_gradients():
207
+ """Return {mode_key: ('#rrggbb', '#rrggbb')} gradients."""
208
+ out = {}
209
+ for k, v in MODE_META.items():
210
+ g = v.get("gradient") or (v["accent"], v["accent"])
211
+ out[k] = (_rgb_to_hex(g[0]), _rgb_to_hex(g[1]))
212
+ return out
213
+
214
+ def set_mode_color(mode_key, hex_color):
215
+ """Set a mode's accent to hex_color ('#rrggbb' or 'rrggbb'), auto-
216
+ regenerates its gradient, persists to config, and returns normalized
217
+ hex or None if invalid/mode unknown. Triggers no UI repaint itself —
218
+ callers should call theme_css refresh after."""
219
+ if mode_key not in MODE_META:
220
+ return None
221
+ rgb = _hex_to_rgb(hex_color)
222
+ if rgb is None:
223
+ return None
224
+ MODE_META[mode_key]["accent"] = rgb
225
+ MODE_META[mode_key]["gradient"] = _auto_gradient(rgb)
226
+ _persist_custom_colors()
227
+ if _is_snapshot_active():
228
+ _persist_user_modes_snapshot()
229
+ return _rgb_to_hex(rgb)
230
+
231
+ def set_mode_gradient(mode_key, start_hex, end_hex):
232
+ """Explicitly set both gradient stops for a mode. Returns True on
233
+ success, False if invalid. Persists to config."""
234
+ if mode_key not in MODE_META:
235
+ return False
236
+ s = _hex_to_rgb(start_hex); e = _hex_to_rgb(end_hex)
237
+ if s is None or e is None:
238
+ return False
239
+ MODE_META[mode_key]["gradient"] = (s, e)
240
+ _persist_custom_colors()
241
+ if _is_snapshot_active():
242
+ _persist_user_modes_snapshot()
243
+ return True
244
+
245
+ def reset_mode_color(mode_key):
246
+ """Reset single mode to factory default. Returns True if reset."""
247
+ if mode_key not in _DEFAULT_MODE_META:
248
+ # custom mode: reset to its own creation? Just return False
249
+ return False
250
+ MODE_META[mode_key] = _copy.deepcopy(_DEFAULT_MODE_META[mode_key])
251
+ _persist_custom_colors()
252
+ if _is_snapshot_active():
253
+ _persist_user_modes_snapshot()
254
+ return True
255
+
256
+ def reset_all_mode_colors():
257
+ """Reset all modes to factory defaults."""
258
+ for k in list(MODE_META.keys()):
259
+ if k in _DEFAULT_MODE_META:
260
+ MODE_META[k] = _copy.deepcopy(_DEFAULT_MODE_META[k])
261
+ _persist_custom_colors()
262
+ # also sync snapshot if exists
263
+ try:
264
+ from . import config as _cfg
265
+ cfg = _cfg.load_config()
266
+ if getattr(cfg, "user_modes", None):
267
+ _persist_user_modes_snapshot()
268
+ except Exception:
269
+ pass
270
+ return True
271
+
272
+ # --- FULL MODE SNAPSHOT PERSISTENCE (v0.7.9.8) ---
273
+ def _serialize_current_modes():
274
+ out = {}
275
+ for k, v in MODE_META.items():
276
+ grad = v.get("gradient")
277
+ out[k] = {
278
+ "label": v.get("label", k.title()),
279
+ "icon": v.get("icon", "●"),
280
+ "accent": _rgb_to_hex(v.get("accent", (128,128,128))),
281
+ "gradient": [_rgb_to_hex(grad[0]), _rgb_to_hex(grad[1])] if grad and len(grad)==2 else None,
282
+ "purpose": v.get("purpose", ""),
283
+ "system_prompt": v.get("system_prompt", ""),
284
+ }
285
+ return out
286
+
287
+ def _persist_user_modes_snapshot():
288
+ """Snapshot current MODE_META+MODE_ORDER into config.user_modes/order."""
289
+ try:
290
+ from . import config as _cfg
291
+ cfg = _cfg.load_config()
292
+ cfg.user_modes = _serialize_current_modes()
293
+ cfg.user_mode_order = list(MODE_ORDER)
294
+ _cfg.save_config(cfg)
295
+ except Exception:
296
+ pass
297
+
298
+ def _apply_user_modes_snapshot():
299
+ """If config has a full snapshot, replace MODE_META/MODE_ORDER with it."""
300
+ try:
301
+ from . import config as _cfg
302
+ cfg = _cfg.load_config()
303
+ snap = getattr(cfg, "user_modes", None) or {}
304
+ order = getattr(cfg, "user_mode_order", None) or []
305
+ if not isinstance(snap, dict) or not snap:
306
+ return False
307
+ # validate snapshot: each entry needs accent
308
+ new_meta = {}
309
+ for k, d in snap.items():
310
+ if not isinstance(d, dict) or not isinstance(k, str):
311
+ continue
312
+ accent_hex = d.get("accent")
313
+ rgb = _hex_to_rgb(accent_hex) if accent_hex else None
314
+ if rgb is None:
315
+ continue
316
+ grad = d.get("gradient")
317
+ if isinstance(grad, (list, tuple)) and len(grad)==2:
318
+ g0 = _hex_to_rgb(grad[0]); g1 = _hex_to_rgb(grad[1])
319
+ gradient = (g0, g1) if g0 and g1 else _auto_gradient(rgb)
320
+ else:
321
+ gradient = _auto_gradient(rgb)
322
+ new_meta[k] = {
323
+ "label": str(d.get("label", k.title()))[:24],
324
+ "icon": str(d.get("icon", "●"))[:4],
325
+ "accent": rgb,
326
+ "gradient": gradient,
327
+ "purpose": str(d.get("purpose", ""))[:200],
328
+ "system_prompt": str(d.get("system_prompt", ""))[:2000],
329
+ }
330
+ if not new_meta:
331
+ return False
332
+ # apply
333
+ MODE_META.clear()
334
+ MODE_META.update(new_meta)
335
+ if isinstance(order, list) and order:
336
+ # keep only keys that exist and preserve order, append any missing
337
+ filtered = [k for k in order if k in new_meta]
338
+ for k in new_meta:
339
+ if k not in filtered:
340
+ filtered.append(k)
341
+ MODE_ORDER[:] = filtered
342
+ else:
343
+ MODE_ORDER[:] = list(new_meta.keys())
344
+ return True
345
+ except Exception:
346
+ return False
347
+
348
+ def _is_snapshot_active():
349
+ try:
350
+ from . import config as _cfg
351
+ snap = getattr(_cfg.load_config(), "user_modes", None)
352
+ return isinstance(snap, dict) and bool(snap)
353
+ except Exception:
354
+ return False
355
+
356
+ # --- full CRUD (v0.7.9.8) ---
357
+ import re as _re_key
358
+ _KEY_RE = _re_key.compile(r"^[a-z0-9_]{2,20}$")
359
+
360
+ def _valid_key(k):
361
+ return bool(_KEY_RE.match(k or ""))
362
+
363
+ def create_mode(key, label, icon, accent_hex, purpose="", system_prompt=""):
364
+ """Create a new custom mode. Key must be 2-20 [a-z0-9_], not existing.
365
+ Returns (True, key) or (False, error). Persists snapshot."""
366
+ key = (key or "").strip().lower()
367
+ if not _valid_key(key):
368
+ return False, "Key must be 2-20 chars: a-z, 0-9, _"
369
+ if key in MODE_META:
370
+ return False, f"Mode '{key}' already exists"
371
+ rgb = _hex_to_rgb(accent_hex)
372
+ if rgb is None:
373
+ return False, "Invalid hex, use #rrggbb"
374
+ label = (label or key).strip()[:24] or key.title()
375
+ icon = (icon or "●").strip()[:4] or "●"
376
+ purpose = (purpose or "").strip()[:200]
377
+ system_prompt = (system_prompt or "").strip()[:2000]
378
+ MODE_META[key] = {
379
+ "label": label, "icon": icon, "accent": rgb,
380
+ "gradient": _auto_gradient(rgb),
381
+ "purpose": purpose, "system_prompt": system_prompt,
382
+ }
383
+ if key not in MODE_ORDER:
384
+ MODE_ORDER.append(key)
385
+ _persist_user_modes_snapshot()
386
+ # also keep accent_colors in sync for backward compat
387
+ try:
388
+ _persist_custom_colors()
389
+ except Exception:
390
+ pass
391
+ return True, key
392
+
393
+ def update_mode(key, label=None, icon=None, accent_hex=None, purpose=None, system_prompt=None):
394
+ """Update fields of an existing mode. None = leave unchanged.
395
+ Returns True/False."""
396
+ if key not in MODE_META:
397
+ return False
398
+ m = MODE_META[key]
399
+ if label is not None:
400
+ m["label"] = str(label).strip()[:24] or m["label"]
401
+ if icon is not None:
402
+ m["icon"] = str(icon).strip()[:4] or m["icon"]
403
+ if accent_hex is not None:
404
+ rgb = _hex_to_rgb(accent_hex)
405
+ if rgb is None:
406
+ return False
407
+ m["accent"] = rgb
408
+ m["gradient"] = _auto_gradient(rgb)
409
+ if purpose is not None:
410
+ m["purpose"] = str(purpose).strip()[:200]
411
+ if system_prompt is not None:
412
+ m["system_prompt"] = str(system_prompt).strip()[:2000]
413
+ _persist_user_modes_snapshot()
414
+ try:
415
+ _persist_custom_colors()
416
+ except Exception:
417
+ pass
418
+ return True
419
+
420
+ def delete_mode(key):
421
+ """Delete a mode. Must keep at least 1 mode. Returns (True, msg) or (False, err)."""
422
+ if key not in MODE_META:
423
+ return False, "Not found"
424
+ if len(MODE_META) <= 1:
425
+ return False, "Cannot delete the last mode"
426
+ # if deleting current, switch
427
+ global _current
428
+ was_current = (_current == key)
429
+ del MODE_META[key]
430
+ if key in MODE_ORDER:
431
+ MODE_ORDER.remove(key)
432
+ if was_current:
433
+ _current = MODE_ORDER[0] if MODE_ORDER else "notebook"
434
+ try:
435
+ _persist_current()
436
+ except Exception:
437
+ pass
438
+ _persist_user_modes_snapshot()
439
+ try:
440
+ _persist_custom_colors()
441
+ except Exception:
442
+ pass
443
+ return True, f"Deleted '{key}'"
444
+
445
+ def reorder_modes(new_order):
446
+ """Reorder MODE_ORDER to match new_order list (must contain same keys)."""
447
+ if set(new_order) != set(MODE_META.keys()) or len(new_order) != len(MODE_META):
448
+ return False
449
+ MODE_ORDER[:] = list(new_order)
450
+ _persist_user_modes_snapshot()
451
+ return True
452
+
453
+ def move_mode(key, direction):
454
+ """Move mode up (-1) or down (+1) in order. Returns True if moved."""
455
+ if key not in MODE_ORDER:
456
+ return False
457
+ idx = MODE_ORDER.index(key)
458
+ new_idx = idx + direction
459
+ if 0 <= new_idx < len(MODE_ORDER):
460
+ MODE_ORDER[idx], MODE_ORDER[new_idx] = MODE_ORDER[new_idx], MODE_ORDER[idx]
461
+ _persist_user_modes_snapshot()
462
+ return True
463
+ return False
464
+
465
+ def reset_all_modes():
466
+ """Reset to factory defaults: clear snapshot and restore defaults."""
467
+ global MODE_META, MODE_ORDER
468
+ MODE_META.clear()
469
+ for k, v in _DEFAULT_MODE_META.items():
470
+ MODE_META[k] = _copy.deepcopy(v)
471
+ MODE_ORDER[:] = ["notebook", "research", "plan", "build", "debugger", "agent"]
472
+ try:
473
+ from . import config as _cfg
474
+ cfg = _cfg.load_config()
475
+ cfg.user_modes = {}
476
+ cfg.user_mode_order = []
477
+ # also reset colors
478
+ cfg.accent_colors = {k: _rgb_to_hex(v["accent"]) for k, v in _DEFAULT_MODE_META.items()}
479
+ grads = {k: [_rgb_to_hex(v["gradient"][0]), _rgb_to_hex(v["gradient"][1])] for k, v in _DEFAULT_MODE_META.items()}
480
+ cfg.accent_gradients = grads
481
+ _cfg.save_config(cfg)
482
+ except Exception:
483
+ pass
484
+ return True
485
+
486
+ _current = "notebook"
487
+
488
+
489
+ def _restore_saved_mode():
490
+ """v0.7.8.2 (mode-persistence fix): the selected AI mode is user
491
+ state, not a per-launch default — read the saved mode back from the
492
+ central config (~/.cct_config.json) once at import so a restart
493
+ never silently drops the user back to Notebook. Falls back to the
494
+ built-in "notebook" default when config is missing/corrupt."""
495
+ global _current
496
+ try:
497
+ from . import config as _cfg
498
+ saved = _cfg.load_config().get("default_ai_mode")
499
+ if saved in MODE_META:
500
+ _current = saved
501
+ except Exception:
502
+ pass
503
+
504
+
505
+ def _persist_current():
506
+ """Best-effort write of the active mode into the central config so
507
+ the next launch restores it (see _restore_saved_mode). Never
508
+ raises — a read-only home directory just means the mode isn't
509
+ remembered across restarts."""
510
+ try:
511
+ from . import config as _cfg
512
+ cfg = _cfg.load_config()
513
+ if cfg.get("default_ai_mode") != _current:
514
+ cfg.set("default_ai_mode", _current)
515
+ _cfg.save_config(cfg)
516
+ except Exception:
517
+ pass
518
+
519
+
520
+ # v0.7.9.8 startup: snapshot has priority over legacy accent_colors, then restore current mode
521
+ if not _apply_user_modes_snapshot():
522
+ _apply_custom_colors()
523
+ _restore_saved_mode()
524
+
525
+
526
+ def current_mode():
527
+ return _current
528
+
529
+
530
+ def set_mode(key):
531
+ """Sets the active mode if valid; returns the (possibly unchanged)
532
+ current mode. Callers that need to know whether it actually
533
+ changed should compare against current_mode() beforehand.
534
+ v0.7.8.2: a successful switch is persisted to the central config
535
+ (best-effort) so the user's choice survives restarts."""
536
+ global _current
537
+ if key in MODE_META:
538
+ _current = key
539
+ _persist_current()
540
+ return _current
541
+
542
+
543
+ def next_mode(after=None):
544
+ """Cycles to the next mode in MODE_ORDER after `after` (or the
545
+ current mode if omitted). Does not mutate state — callers decide
546
+ whether to set_mode() the result."""
547
+ base = after if after in MODE_META else _current
548
+ i = MODE_ORDER.index(base)
549
+ return MODE_ORDER[(i + 1) % len(MODE_ORDER)]
550
+
551
+
552
+ def meta(key=None):
553
+ return MODE_META.get(key or _current, MODE_META["notebook"])
554
+
555
+
556
+ def label(key=None):
557
+ return meta(key)["label"]
558
+
559
+
560
+ def icon(key=None):
561
+ return meta(key)["icon"]
562
+
563
+
564
+ def accent_rgb(key=None):
565
+ return meta(key)["accent"]
566
+
567
+
568
+ def accent_hex(key=None):
569
+ r, g, b = accent_rgb(key)
570
+ return f"#{r:02x}{g:02x}{b:02x}"
571
+
572
+
573
+ def gradient(key=None):
574
+ """Two-stop (start_hex, end_hex) gradient for a mode (v0.7.8,
575
+ FEATURE 3). Falls back to a self-blend of the accent when a mode
576
+ has no explicit gradient, so no mode ever renders flat."""
577
+ k = key if key in MODE_META else _current
578
+ m = MODE_META[k]
579
+ g = m.get("gradient") or (m["accent"], m["accent"])
580
+ return f"#{g[0][0]:02x}{g[0][1]:02x}{g[0][2]:02x}", \
581
+ f"#{g[1][0]:02x}{g[1][1]:02x}{g[1][2]:02x}"
582
+
583
+
584
+ def snapshot(key=None):
585
+ """Returns a small, fully-resolved, *plain-dict* rendering snapshot
586
+ for a mode: label, icon, accent hex/rgb. This is the one function
587
+ anything wanting a message-level, permanent record of "what mode
588
+ was this created in" should call — and it should be called exactly
589
+ once, at the moment the message/turn is created (see
590
+ session.Turn.__init__).
591
+
592
+ Callers must NOT hang onto `key` and re-call meta()/accent_hex()
593
+ later expecting the same answer — `_current` is a mutable global
594
+ that keeps moving. A `snapshot()` dict, once made, is a plain copy
595
+ with no ties back to this module, so it can be stashed on a Turn
596
+ (or anywhere else) and stay correct forever, independent of every
597
+ later set_mode() call in the same process."""
598
+ k = key if key in MODE_META else _current
599
+ m = MODE_META[k]
600
+ return {
601
+ "mode": k,
602
+ "label": m["label"],
603
+ "icon": m["icon"],
604
+ "accent_rgb": tuple(m["accent"]),
605
+ "accent_hex": accent_hex(k),
606
+ }
607
+
608
+
609
+ def alias_for(mode_key):
610
+ """The slash-command spelling that switches directly to this mode
611
+ (see ui/app.py's `_handle_command` — `/agent`, `/build`, `/plan`,
612
+ `/research`, `/debug` and bare `/notebook` all resolve here instead
613
+ of the old suspend-to-terminal behavior)."""
614
+ return {"notebook": "/notebook", "agent": "/agent", "build": "/build",
615
+ "plan": "/plan", "research": "/research",
616
+ "debugger": "/debug"}.get(mode_key)
617
+
618
+
619
+ # ------------------------------------------------------- system prompts --
620
+ _BUILD_SYSTEM_PROMPT = (
621
+ "You are CAT Build, the software-development persona of CAT AI "
622
+ "(Coding Agent Terminal), embedded inside the same chat as CAT's "
623
+ "chemistry/quantum tools. In Build mode the user wants working "
624
+ "software, not a chemistry calculation: code generation, debugging, "
625
+ "architecture and design decisions, deployment planning, and "
626
+ "hands-on help across HTML, Python, C++, Java, JavaScript, and API "
627
+ "design.\n\n"
628
+ "Write real, runnable code in fenced code blocks with the correct "
629
+ "language tag. Prefer complete, working examples over fragments; "
630
+ "call out any assumptions or missing pieces plainly instead of "
631
+ "silently inventing behavior. When debugging, identify the root "
632
+ "cause before proposing a fix, the same way this app's own "
633
+ "changelogs do. Keep chemistry/quantum tool talk out of this mode "
634
+ "unless the user's software is itself about chemistry/quantum "
635
+ "computing (e.g. a Qiskit script) — that's fine and welcome.\n\n"
636
+ + identity.IDENTITY_BLOCK
637
+ )
638
+
639
+ _PLAN_SYSTEM_PROMPT = (
640
+ "You are CAT Plan, the planning persona of CAT AI (Coding Agent "
641
+ "Terminal). In Plan mode the user wants structure for something "
642
+ "that doesn't exist yet: project roadmaps, brainstorming, early "
643
+ "concepts and prototypes, research plans, study plans, or startup "
644
+ "ideas.\n\n"
645
+ "Favor concrete, ordered steps and clear milestones over vague "
646
+ "encouragement. Surface trade-offs and open questions explicitly "
647
+ "rather than picking one path silently. Where useful, structure "
648
+ "the answer as numbered phases or a short roadmap rather than one "
649
+ "long paragraph. You are not expected to execute anything here "
650
+ "(no tool calls, no simulations) — that's Agent mode's job; Plan "
651
+ "mode is about deciding what to do, not doing it.\n\n"
652
+ "For greetings like hi/hello/hey, respond ONLY as: Hi, I'm CAT Plan — your planning assistant inside Coding Agent Terminal by Kazi Zillani. How can I help you plan today? Do not say \"My name is your assistant\" and do not output documentation, training data, or code unless the user asks for it.\n\n"
653
+ "For identity questions (who are you, what are you, who made you, where are you), "
654
+ "answer ONLY with the CAT identity: CAT (Coding Agent Terminal) by "
655
+ "Kazi Zillani — never claim to be \"your assistant\" or generic assistant, never reveal "
656
+ "install code for other projects like `pip install assistant_chatbot --user` or Assistant-Chatbot (MohitSaini1590), "
657
+ "and never output ```##[2]Install the library``` type training data. If you are about to write `My name is your assistant` or `pip install assistant_chatbot`, stop and instead write the CAT identity.\n\n"
658
+ "When you include code, always use correct Python syntax with proper "
659
+ "spaces, newlines, and indentation. Wrap code in fenced blocks with "
660
+ "language tags (```python). Never concatenate keywords (use `import sys` "
661
+ "not `importsys`, `from PyQt5` not `fromPyQt5`). Use standard ASCII "
662
+ "identifiers only (use `xData1` not `xData\u2081`). Preserve 4-space "
663
+ "indentation and blank lines between logical blocks. Always separate "
664
+ "prose from code fences with a blank line (use `hey\\n\\n```python` "
665
+ "not `hey```python`).\n\n"
666
+ + identity.IDENTITY_BLOCK
667
+ )
668
+
669
+ _RESEARCH_SYSTEM_PROMPT = (
670
+ "You are CAT Research, the research persona of CAT AI (Coding "
671
+ "Calc Terminal). In Research mode the user wants verified, current "
672
+ "information: documentation search, API research, framework "
673
+ "comparisons, web research, and deep analysis.\n\n"
674
+ "Prefer citing your sources (the web_search and deep_research tools "
675
+ "return real URLs — name them) over asserting training-memory facts "
676
+ "as current. When you cannot verify something live, say so plainly "
677
+ "instead of hedging. Structure long answers with short headers and "
678
+ "a comparison table or bullet list where one fits. You are not "
679
+ "expected to install or build anything here — that's Build mode's "
680
+ "job.\n\n"
681
+ + identity.IDENTITY_BLOCK
682
+ )
683
+
684
+ _DEBUGGER_SYSTEM_PROMPT = (
685
+ "You are CAT Debugger, the diagnosis persona of CAT AI (Coding "
686
+ "Calc Terminal). In Debugger mode the user wants problems solved "
687
+ "precisely: error analysis, crash investigation, performance "
688
+ "profiling, and security analysis.\n\n"
689
+ "Identify the root cause before proposing a fix — reproduce the "
690
+ "failure chain (input → expected → actual), separate symptoms from "
691
+ "causes, and only then suggest changes. Prefer concrete, minimal "
692
+ "fixes with reasoning over rewrites. For security findings, label "
693
+ "severity honestly (high/medium/low) and never claim something is "
694
+ "vulnerable without a concrete mechanism. Keep the answer structured "
695
+ "with short headers.\n\n"
696
+ + identity.IDENTITY_BLOCK
697
+ )
698
+
699
+
700
+ def system_prompt_for(mode_key):
701
+ """Returns the plain (non-tool-loop) system prompt for a mode.
702
+ Only meaningful for "notebook"/"build"/"plan"/"research"/"debugger"
703
+ — "agent" doesn't use this at all, it goes through
704
+ agent.run_agent()'s own AGENT_SYSTEM_PROMPT instead (see
705
+ ui/app.py's _stream_worker).
706
+
707
+ v0.7.8 AI Personalization: the active profile's style directive is
708
+ appended here, so every mode prompt honors the user's tone / rules
709
+ without touching the mode prompts themselves."""
710
+ if mode_key == "build":
711
+ base = _BUILD_SYSTEM_PROMPT
712
+ elif mode_key == "plan":
713
+ base = _PLAN_SYSTEM_PROMPT
714
+ elif mode_key == "research":
715
+ base = _RESEARCH_SYSTEM_PROMPT
716
+ elif mode_key == "debugger":
717
+ base = _DEBUGGER_SYSTEM_PROMPT
718
+ else:
719
+ # Check if this is a custom mode in mode_registry
720
+ try:
721
+ from .core.mode_registry import mode_registry
722
+ custom_spec = mode_registry.get_mode(mode_key)
723
+ if custom_spec:
724
+ if custom_spec.system_prompt:
725
+ base = custom_spec.system_prompt
726
+ else:
727
+ parent = custom_spec.inherits[0] if custom_spec.inherits else "notebook"
728
+ base = system_prompt_for(parent)
729
+ # Inject custom mode capabilities guidance
730
+ if custom_spec.capabilities:
731
+ caps_list = ", ".join(custom_spec.capabilities)
732
+ base += f"\n\n[Active Mode: {custom_spec.name}]\nAllowed capabilities: {caps_list}."
733
+ else:
734
+ from . import aicore
735
+ base = aicore.DEFAULT_SYSTEM_PROMPT
736
+ except Exception:
737
+ from . import aicore
738
+ base = aicore.DEFAULT_SYSTEM_PROMPT
739
+ try:
740
+ from . import ai_personalization as ap
741
+ return ap.personalize_system_prompt(base)
742
+ except Exception:
743
+ return base
744
+
745
+
746
+ # Sync any registered custom modes on startup
747
+ try:
748
+ from .core.mode_registry import mode_registry as _mr
749
+ _mr._sync_with_ai_modes()
750
+ except Exception:
751
+ pass
752
+