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,517 @@
1
+ """
2
+ CCT UI — PersonalizationPanel (v0.7.8.1 full redesign).
3
+
4
+ Professional management screen for AI personalization profiles
5
+ (calc_terminal/ai_personalization.py). The Main Menu's "Personalize"
6
+ destination routes here.
7
+
8
+ Layout (per the v0.7.8.1 spec):
9
+ left — profile list (name · tone · active badge · last modified)
10
+ + New / Duplicate / Delete / Set Active
11
+ right — the full editor: Name, Description, Tone, Temperature,
12
+ Top P, System prompt rules, Creativity, Reasoning,
13
+ Response Length, Memory Preference
14
+ footer — Active Profile · Last Modified · Provider (model label)
15
+
16
+ Honesty note: temperature, top_p, tone and the system prompt rules
17
+ are applied for real (temperature/top_p reach the provider payloads;
18
+ tone + rules reach every system prompt). Creativity, Reasoning,
19
+ Response Length and Memory Preference are stored as profile metadata
20
+ and surfaced here + in the dashboard — they describe the profile for
21
+ future wiring and are never fake "settings" that do nothing silently.
22
+ """
23
+
24
+ if __name__ == "__main__":
25
+ print("This is a library file and is not meant to be run directly.")
26
+ import sys
27
+ sys.exit(1)
28
+
29
+ import time
30
+
31
+ TEXTUAL_AVAILABLE = True
32
+ try:
33
+ from textual.screen import Screen
34
+ from textual.containers import Vertical, Horizontal, ScrollableContainer
35
+ from textual.widgets import Static, Button, Input
36
+ from textual.binding import Binding
37
+ except Exception:
38
+ TEXTUAL_AVAILABLE = False
39
+
40
+ if TEXTUAL_AVAILABLE:
41
+ from .. import ai_personalization as ap
42
+ from . import theme_css
43
+
44
+ _TONE_BTNS = (
45
+ ("balanced", "Balanced"),
46
+ ("concise", "Concise"),
47
+ ("detailed", "Detailed"),
48
+ ("playful", "Playful"),
49
+ ("formal", "Formal"),
50
+ )
51
+
52
+ _SEG_BTNS = {
53
+ "pp-reasoning": (("low", "Low"), ("medium", "Medium"), ("high", "High")),
54
+ "pp-length": (("short", "Short"), ("medium", "Medium"), ("long", "Long")),
55
+ "pp-memory": (("auto", "Auto"), ("on", "On"), ("off", "Off")),
56
+ }
57
+
58
+
59
+ class _ProfileRow(Static):
60
+ """One row of the profile list: name · tone · active badge · last modified."""
61
+
62
+ def __init__(self, index, profile, selected=False):
63
+ self.index = index
64
+ self.profile = profile
65
+ super().__init__("", classes="pp-row" + (" pp-row-sel" if selected else ""))
66
+ self._redraw()
67
+
68
+ def _redraw(self):
69
+ name = self.profile.get("name") or "?"
70
+ tone = self.profile.get("tone") or "balanced"
71
+ active = self.profile.get("_active")
72
+ updated = self.profile.get("updated_at") or 0
73
+ when = time.strftime("%Y-%m-%d", time.localtime(updated)) if updated else "\u2014"
74
+ badge = ""
75
+ if active:
76
+ badge = f" [{theme_css.current_hex('accent')}]\u2605 active[/]"
77
+ sel = " \u25b8" if "pp-row-sel" in self.classes else ""
78
+ self.update(
79
+ f" [{theme_css.current_hex('text')}]{name}[/]{badge}{sel}\n"
80
+ f" [{theme_css.current_hex('text-faint')}]{tone} \u00b7 modified {when}[/]")
81
+
82
+ def set_selected(self, selected):
83
+ self.set_class(selected, "pp-row-sel")
84
+ self._redraw()
85
+
86
+
87
+ class PersonalizationPanel(Screen):
88
+ """Manage AI personalization profiles: create, edit, duplicate,
89
+ delete, pick the active one — clean ChatGPT-style layout."""
90
+
91
+ CSS = """
92
+ PersonalizationPanel { align: center middle; background: $app-background 70%; }
93
+ #pp-box {
94
+ width: 90; max-width: 95%; height: auto; max-height: 38;
95
+ background: $surface; border: round $border; padding: 0;
96
+ opacity: 0; offset-y: 1;
97
+ transition: opacity 150ms, offset 180ms;
98
+ layout: vertical;
99
+ }
100
+ #pp-box.open { opacity: 1; offset-y: 0; }
101
+ #pp-titlebar {
102
+ height: 3; min-height: 3; padding: 1 2 0 2;
103
+ border-bottom: solid $border;
104
+ }
105
+ #pp-title { text-style: bold; width: 1fr; }
106
+ #pp-subtitle { color: $text-faint; height: 1; min-height: 1; padding: 0 2; }
107
+ #pp-body { height: 1fr; min-height: 0; layout: horizontal; }
108
+ #pp-sidebar { width: 24; min-width: 24; height: 100%; padding: 1 0 1 1; }
109
+ #pp-list-scroll { width: 100%; height: 100%; min-height: 0;
110
+ overflow-y: auto; scrollbar-gutter: stable; }
111
+ .pp-row { height: 2; color: $text-muted; padding: 0 1; border-left: thick transparent; }
112
+ .pp-row-sel { color: $text; border-left: thick $accent; }
113
+ .pp-empty { color: $text-faint; padding: 2 1; }
114
+ #pp-edit { width: 1fr; height: 100%; min-height: 0; padding: 1 2;
115
+ overflow-y: auto; scrollbar-gutter: stable;
116
+ scrollbar-size-vertical: 1; scrollbar-size-horizontal: 0;
117
+ border-left: solid $border; }
118
+ .pp-seg { color: $accent; height: auto; min-height: 1; padding-top: 1;
119
+ border-top: solid $border; margin-top: 1; }
120
+ .pp-field { height: auto; min-height: 1; margin-bottom: 1; }
121
+ .pp-flbl { color: $text-faint; height: 1; min-height: 1; }
122
+ #pp-tone-row { height: 3; min-height: 3; padding: 0; }
123
+ #pp-tone-row Button { margin-right: 1; }
124
+ #pp-reasoning, #pp-length, #pp-memory { height: 3; min-height: 3; padding: 0; }
125
+ #pp-reasoning Button, #pp-length Button, #pp-memory Button { margin-right: 1; }
126
+ #pp-btns {
127
+ height: auto; min-height: 3; padding: 1 2;
128
+ border-top: solid $border;
129
+ }
130
+ #pp-btns-left { width: 1fr; }
131
+ #pp-btns-left Button { margin-right: 1; }
132
+ #pp-btns-right Button { margin-left: 1; }
133
+ #pp-hint { color: $text-faint; height: 1; min-height: 1; padding: 0 2; }
134
+ """
135
+
136
+ BINDINGS = [
137
+ Binding("escape", "cancel", "Cancel"),
138
+ Binding("down", "move_down_sel", "Next profile"),
139
+ Binding("up", "move_up_sel", "Previous profile"),
140
+ Binding("page_down", "page_down_edit", "Page down"),
141
+ Binding("page_up", "page_up_edit", "Page up"),
142
+ Binding("home", "home_edit", "Top of form"),
143
+ Binding("end", "end_edit", "Bottom of form"),
144
+ ]
145
+
146
+ def __init__(self):
147
+ super().__init__()
148
+ self._profiles = ap.profiles()
149
+ self._active = ap.load_profiles().get("active", "")
150
+ self._selected = 0
151
+
152
+ def compose(self):
153
+ with Vertical(id="pp-box"):
154
+ with Horizontal(id="pp-titlebar"):
155
+ yield Static("\u2726 Personalize", id="pp-title")
156
+ yield Button("\u2715", id="pp-close", classes="cct-popup-close")
157
+ yield Static(
158
+ "Customize how the AI responds to you \u00b7 tone, style, behavior.",
159
+ id="pp-subtitle")
160
+ with Horizontal(id="pp-body"):
161
+ with Vertical(id="pp-sidebar"):
162
+ yield Static("My Profiles", classes="pp-seg")
163
+ yield ScrollableContainer(id="pp-list-scroll")
164
+ with ScrollableContainer(id="pp-edit"):
165
+ yield Static("Profile Settings", classes="pp-seg")
166
+ with Vertical(classes="pp-field"):
167
+ yield Static("Name", classes="pp-flbl")
168
+ yield Input(id="pp-name", placeholder="Profile name")
169
+ with Vertical(classes="pp-field"):
170
+ yield Static("What should the AI know about you?", classes="pp-flbl")
171
+ yield Input(id="pp-desc", placeholder="e.g. I'm a software engineer")
172
+ yield Static("Tone", classes="pp-seg")
173
+ with Horizontal(id="pp-tone-row"):
174
+ for tone, label in _TONE_BTNS:
175
+ yield Button(label, id=f"pp-tone-{tone}", classes="cct-btn cct-btn-sm")
176
+ yield Static("Model Parameters", classes="pp-seg")
177
+ with Horizontal():
178
+ with Vertical(classes="pp-field"):
179
+ yield Static("Reasoning", classes="pp-flbl")
180
+ with Horizontal(id="pp-reasoning"):
181
+ for key, label in _SEG_BTNS["pp-reasoning"]:
182
+ yield Button(label, id=f"pp-reasoning-{key}",
183
+ classes="cct-btn cct-btn-sm")
184
+ with Vertical(classes="pp-field"):
185
+ yield Static("Length", classes="pp-flbl")
186
+ with Horizontal(id="pp-length"):
187
+ for key, label in _SEG_BTNS["pp-length"]:
188
+ yield Button(label, id=f"pp-length-{key}",
189
+ classes="cct-btn cct-btn-sm")
190
+ with Horizontal():
191
+ with Vertical(classes="pp-field"):
192
+ yield Static("Temperature (0\u20132)", classes="pp-flbl")
193
+ yield Input(id="pp-temp", placeholder="0.7")
194
+ with Vertical(classes="pp-field"):
195
+ yield Static("Top P (0\u20131)", classes="pp-flbl")
196
+ yield Input(id="pp-topp", placeholder="0.9")
197
+ yield Static("Behavior", classes="pp-seg")
198
+ with Vertical(classes="pp-field"):
199
+ yield Static("Memory", classes="pp-flbl")
200
+ with Horizontal(id="pp-memory"):
201
+ for key, label in _SEG_BTNS["pp-memory"]:
202
+ yield Button(label, id=f"pp-memory-{key}",
203
+ classes="cct-btn cct-btn-sm")
204
+ with Vertical(classes="pp-field"):
205
+ yield Static("Custom instructions", classes="pp-flbl")
206
+ yield Input(id="pp-additions", placeholder="e.g. always use SI units")
207
+ with Horizontal():
208
+ with Vertical(classes="pp-field"):
209
+ yield Static("Creativity (0\u20131)", classes="pp-flbl")
210
+ yield Input(id="pp-creativity", placeholder="0.7")
211
+ with Vertical(classes="pp-field"):
212
+ yield Static("Provider / Model", classes="pp-flbl")
213
+ yield Input(id="pp-model", placeholder="gpt-4o-cct")
214
+ with Horizontal(id="pp-btns"):
215
+ with Horizontal(id="pp-btns-left"):
216
+ yield Button(" + New ", id="pp-new", classes="cct-btn cct-btn-sm")
217
+ yield Button(" Duplicate ", id="pp-duplicate", classes="cct-btn cct-btn-sm")
218
+ yield Button(" Delete ", id="pp-delete", variant="error", classes="cct-btn cct-btn-sm")
219
+ yield Button(" Set Active ", id="pp-active", classes="cct-btn cct-btn-sm")
220
+ with Horizontal(id="pp-btns-right"):
221
+ yield Button(" Save ", id="pp-save", variant="primary", classes="cct-btn cct-btn-sm")
222
+ yield Button(" Cancel ", id="pp-cancel", classes="cct-btn cct-btn-sm")
223
+ yield Static("Up/Down navigate \u00b7 Esc close", id="pp-hint")
224
+
225
+ # ------------------------------------------------------------ rows
226
+ def _rows(self):
227
+ self._profiles = ap.profiles()
228
+ active = self._active
229
+ out = []
230
+ for i, prof in enumerate(self._profiles):
231
+ p = dict(prof)
232
+ p["_active"] = p.get("name") == active
233
+ out.append(_ProfileRow(i, p, selected=(i == self._selected)))
234
+ return out
235
+
236
+ def _rebuild_list(self):
237
+ try:
238
+ scroll = self.query_one("#pp-list-scroll", ScrollableContainer)
239
+ scroll.remove_children()
240
+ rows = self._rows()
241
+ if not rows:
242
+ scroll.mount(Static("No profiles \u2014 create one.", classes="pp-empty"))
243
+ else:
244
+ for row in rows:
245
+ scroll.mount(row)
246
+ self._sync_form()
247
+ except Exception:
248
+ pass
249
+
250
+ def _fit(self):
251
+ """No-op: CSS handles layout with fixed height."""
252
+ pass
253
+
254
+ def _sync_form(self):
255
+ prof = self._profiles[self._selected] if self._profiles else {}
256
+ self.query_one("#pp-name", Input).value = prof.get("name", "")
257
+ self.query_one("#pp-desc", Input).value = prof.get("description", "")
258
+ self.query_one("#pp-temp", Input).value = (
259
+ "" if prof.get("temperature") is None else str(prof.get("temperature")))
260
+ self.query_one("#pp-topp", Input).value = (
261
+ "" if prof.get("top_p") is None else str(prof.get("top_p")))
262
+ self.query_one("#pp-additions", Input).value = prof.get("additions", "")
263
+ self.query_one("#pp-creativity", Input).value = (
264
+ "" if prof.get("creativity") is None else str(prof.get("creativity")))
265
+ self.query_one("#pp-model", Input).value = prof.get("model", "")
266
+ tone = prof.get("tone") or "balanced"
267
+ for btone, _ in _TONE_BTNS:
268
+ self._set_seg(f"#pp-tone-{btone}", btone == tone)
269
+ seg_defaults = {
270
+ "pp-reasoning": prof.get("reasoning") or "medium",
271
+ "pp-length": prof.get("response_length") or "medium",
272
+ "pp-memory": prof.get("memory_pref") or "auto",
273
+ }
274
+ for row_id, chosen in seg_defaults.items():
275
+ for key, _label in _SEG_BTNS[row_id]:
276
+ self._set_seg(f"#{row_id}-{key}", key == chosen)
277
+
278
+ def _set_seg(self, btn_id, on):
279
+ try:
280
+ self.query_one(btn_id, Button).variant = "primary" if on else "default"
281
+ except Exception:
282
+ pass
283
+
284
+ def _seg_value(self, row_id):
285
+ for key, _label in _SEG_BTNS[row_id]:
286
+ btn = self.query_one(f"#{row_id}-{key}", Button)
287
+ if btn.variant == "primary":
288
+ return key
289
+ return _SEG_BTNS[row_id][1][0]
290
+
291
+ # ------------------------------------------------------- selection
292
+ def _editing(self):
293
+ """True when keyboard focus sits inside the editor column —
294
+ Up/Down then scroll the form instead of switching profiles
295
+ (a stray Down in the Name field must never change the
296
+ selected profile and wipe the form)."""
297
+ focused = self.focused
298
+ if focused is None:
299
+ return False
300
+ try:
301
+ return self.query_one("#pp-edit").is_ancestor_of(focused)
302
+ except Exception:
303
+ return False
304
+
305
+ def _edit_scroll(self):
306
+ try:
307
+ return self.query_one("#pp-edit")
308
+ except Exception:
309
+ return None
310
+
311
+ def action_move_down_sel(self):
312
+ if self._editing():
313
+ scroll = self._edit_scroll()
314
+ if scroll is not None and scroll.max_scroll_y > 0:
315
+ scroll.scroll_down(animate=False)
316
+ return
317
+ if len(self._profiles) > 1:
318
+ self._selected = min(self._selected + 1, len(self._profiles) - 1)
319
+ self._rebuild_list()
320
+
321
+ def action_move_up_sel(self):
322
+ if self._editing():
323
+ scroll = self._edit_scroll()
324
+ if scroll is not None:
325
+ scroll.scroll_up(animate=False)
326
+ return
327
+ self._selected = max(self._selected - 1, 0)
328
+ self._rebuild_list()
329
+
330
+ def action_page_down_edit(self):
331
+ scroll = self._edit_scroll()
332
+ if scroll is not None:
333
+ scroll.scroll_page_down(animate=False)
334
+
335
+ def action_page_up_edit(self):
336
+ scroll = self._edit_scroll()
337
+ if scroll is not None:
338
+ scroll.scroll_page_up(animate=False)
339
+
340
+ def action_home_edit(self):
341
+ scroll = self._edit_scroll()
342
+ if scroll is not None:
343
+ scroll.scroll_home(animate=False)
344
+
345
+ def action_end_edit(self):
346
+ scroll = self._edit_scroll()
347
+ if scroll is not None:
348
+ scroll.scroll_end(animate=False)
349
+
350
+ # ---------------------------------------------------------- actions
351
+ def on_mount(self):
352
+ self.call_after_refresh(lambda: self.query_one("#pp-box").add_class("open"))
353
+ self.call_after_refresh(self._rebuild_list)
354
+
355
+ def on_key(self, event):
356
+ if event.key == "escape":
357
+ self.dismiss(None)
358
+
359
+ def action_cancel(self):
360
+ self.dismiss(None)
361
+
362
+ def on_button_pressed(self, event):
363
+ eid = event.button.id
364
+ if eid in ("pp-cancel", "pp-close"):
365
+ self.dismiss(None)
366
+ elif eid == "pp-new":
367
+ self._new_profile()
368
+ elif eid == "pp-duplicate":
369
+ self._duplicate_profile()
370
+ elif eid == "pp-delete":
371
+ self._delete_profile()
372
+ elif eid == "pp-active":
373
+ self._make_active()
374
+ elif eid == "pp-save":
375
+ self._save()
376
+ self.dismiss(None)
377
+ elif eid and eid.startswith("pp-tone-"):
378
+ tone = eid.split("-", 2)[2]
379
+ for btone, _ in _TONE_BTNS:
380
+ self._set_seg(f"#pp-tone-{btone}", btone == tone)
381
+ elif eid.startswith("pp-reasoning-"):
382
+ self._seg_click("pp-reasoning", eid)
383
+ elif eid.startswith("pp-length-"):
384
+ self._seg_click("pp-length", eid)
385
+ elif eid.startswith("pp-memory-"):
386
+ self._seg_click("pp-memory", eid)
387
+
388
+ def _seg_click(self, row_id, eid):
389
+ key = eid.split("-", 2)[2]
390
+ for k, _label in _SEG_BTNS[row_id]:
391
+ self._set_seg(f"#{row_id}-{k}", k == key)
392
+
393
+ def _new_profile(self):
394
+ base = dict(ap.DEFAULT_PROFILE)
395
+ base["name"] = self._unique_name("New Profile")
396
+ ap.add_profile(base["name"], tone=base["tone"], additions="",
397
+ temperature=None, model="")
398
+ self._profiles = ap.profiles()
399
+ self._selected = len(self._profiles) - 1
400
+ self._rebuild_list()
401
+
402
+ def _unique_name(self, root):
403
+ names = {p.get("name") for p in self._profiles}
404
+ if root not in names:
405
+ return root
406
+ i = 2
407
+ while f"{root} {i}" in names:
408
+ i += 1
409
+ return f"{root} {i}"
410
+
411
+ def _current_prof(self):
412
+ return self._profiles[self._selected] if self._profiles else None
413
+
414
+ def _tone_from_buttons(self):
415
+ tone = "balanced"
416
+ for btone, _ in _TONE_BTNS:
417
+ btn = self.query_one(f"#pp-tone-{btone}", Button)
418
+ if btn.variant == "primary":
419
+ tone = btone
420
+ return tone
421
+
422
+ def _duplicate_profile(self):
423
+ prof = self._current_prof()
424
+ if not prof:
425
+ return
426
+ name = self._unique_name(prof.get("name", "Profile") + " Copy")
427
+ ap.add_profile(name, tone=prof.get("tone", "balanced"),
428
+ additions=prof.get("additions", ""),
429
+ temperature=prof.get("temperature"),
430
+ model=prof.get("model", ""),
431
+ description=prof.get("description", ""),
432
+ top_p=prof.get("top_p"),
433
+ creativity=prof.get("creativity"),
434
+ reasoning=prof.get("reasoning", ""),
435
+ response_length=prof.get("response_length", ""),
436
+ memory_pref=prof.get("memory_pref", ""))
437
+ self._profiles = ap.profiles()
438
+ self._selected = len(self._profiles) - 1
439
+ self._rebuild_list()
440
+
441
+ def _delete_profile(self):
442
+ # save first so in-panel edits aren't lost on the wrong target
443
+ self._save_silently()
444
+ prof = self._current_prof()
445
+ if not prof:
446
+ return
447
+ ap.delete_profile(prof.get("name", ""))
448
+ if self._active == prof.get("name"):
449
+ self._active = ""
450
+ ap.set_active("")
451
+ self._profiles = ap.profiles()
452
+ self._selected = min(self._selected, max(0, len(self._profiles) - 1))
453
+ self._rebuild_list()
454
+
455
+ def _make_active(self):
456
+ prof = self._current_prof()
457
+ if not prof:
458
+ return
459
+ name = prof.get("name", "")
460
+ ap.set_active(name)
461
+ self._active = name
462
+ self._rebuild_list()
463
+ try:
464
+ self.app._system_note(f"\u2726 Personalization: '{name}' is now active.")
465
+ except Exception:
466
+ pass
467
+
468
+ def _save(self):
469
+ self._save_silently()
470
+ try:
471
+ self.app._system_note(
472
+ f"\u2713 Personalization saved \u2014 '{self._active or '(none)'}' is active.")
473
+ except Exception:
474
+ pass
475
+
476
+ def _save_silently(self):
477
+ """Persist the current form fields onto the selected profile
478
+ (or create it if the name is brand new)."""
479
+ name = self.query_one("#pp-name", Input).value.strip()
480
+ if not name:
481
+ return
482
+ temp_raw = self.query_one("#pp-temp", Input).value.strip()
483
+ temperature = None
484
+ if temp_raw:
485
+ try:
486
+ temperature = max(0.0, min(2.0, float(temp_raw)))
487
+ except ValueError:
488
+ temperature = None
489
+ topp_raw = self.query_one("#pp-topp", Input).value.strip()
490
+ top_p = None
491
+ if topp_raw:
492
+ try:
493
+ top_p = max(0.0, min(1.0, float(topp_raw)))
494
+ except ValueError:
495
+ top_p = None
496
+ creat_raw = self.query_one("#pp-creativity", Input).value.strip()
497
+ creativity = None
498
+ if creat_raw:
499
+ try:
500
+ creativity = max(0.0, min(1.0, float(creat_raw)))
501
+ except ValueError:
502
+ creativity = None
503
+ additions = self.query_one("#pp-additions", Input).value
504
+ model = self.query_one("#pp-model", Input).value.strip()
505
+ description = self.query_one("#pp-desc", Input).value.strip()
506
+ tone = self._tone_from_buttons()
507
+ ap.add_profile(name, tone=tone, additions=additions,
508
+ temperature=temperature, model=model,
509
+ description=description, top_p=top_p,
510
+ creativity=creativity,
511
+ reasoning=self._seg_value("pp-reasoning"),
512
+ response_length=self._seg_value("pp-length"),
513
+ memory_pref=self._seg_value("pp-memory"))
514
+ self._profiles = ap.profiles()
515
+
516
+ else:
517
+ PersonalizationPanel = None