cct-cli 0.7.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|
+
|