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,1568 @@
1
+ """
2
+ CCT UI — PersonalizeCenter: personalization hub (v0.8.a) — glitch-fixed.
3
+
4
+ Fixes:
5
+ - Removed buggy `with Vertical(): container.mount()` pattern (mounted to wrong parent)
6
+ - Responsive height: auto + max-height instead of fixed
7
+ - Fixed memory row overflow on narrow terminals
8
+ - Proper mount tree via explicit Horizontal/Vertical objects
9
+ - Clean input handling, profile sync, about_me wiring
10
+ """
11
+
12
+ if __name__ == "__main__":
13
+ print("This is a library file and is not meant to be run directly.")
14
+ import sys
15
+ sys.exit(1)
16
+
17
+ import os
18
+ import time
19
+ import json
20
+
21
+ TEXTUAL_AVAILABLE = True
22
+ try:
23
+ from textual.screen import Screen
24
+ from textual.containers import Vertical, Horizontal, ScrollableContainer
25
+ try:
26
+ from textual.containers import HorizontalScroll
27
+ except Exception:
28
+ HorizontalScroll = Horizontal
29
+ from textual.widgets import Static, Button, Input, Switch, Label
30
+ from textual.binding import Binding
31
+ try:
32
+ from textual.widgets import TextArea
33
+ _HAS_TEXTAREA = True
34
+ except Exception:
35
+ TextArea = None
36
+ _HAS_TEXTAREA = False
37
+ except Exception:
38
+ TEXTUAL_AVAILABLE = False
39
+ _HAS_TEXTAREA = False
40
+
41
+ if TEXTUAL_AVAILABLE:
42
+ try:
43
+ from .. import ai_personalization as ap
44
+ except Exception:
45
+ ap = None
46
+ try:
47
+ from .. import permissions as perm
48
+ except Exception:
49
+ perm = None
50
+ try:
51
+ from .. import memory
52
+ except Exception:
53
+ memory = None
54
+ try:
55
+ from .. import memory_v2
56
+ except Exception:
57
+ memory_v2 = None
58
+ try:
59
+ from . import theme_css
60
+ except Exception:
61
+ theme_css = None
62
+ try:
63
+ from .. import config as cct_config
64
+ except Exception:
65
+ cct_config = None
66
+
67
+ _TONE_BTNS = (
68
+ ("balanced", "Balanced"),
69
+ ("concise", "Concise"),
70
+ ("detailed", "Detailed"),
71
+ ("playful", "Playful"),
72
+ ("formal", "Formal"),
73
+ )
74
+ _SEG_BTNS = {
75
+ "pc-reasoning": (("low", "Low"), ("medium", "Medium"), ("high", "High")),
76
+ "pc-length": (("short", "Short"), ("medium", "Medium"), ("long", "Long")),
77
+ "pc-memory": (("auto", "Auto"), ("on", "On"), ("off", "Off")),
78
+ "pc-teaching": (("direct", "Direct"), ("step_by_step", "Step-by-Step"), ("explanatory", "Explanatory")),
79
+ }
80
+ _NAV_SECTIONS = [
81
+ ("PERSONALIZATION", [
82
+ ("personality", "🎨 AI Personality"),
83
+ ("response_style", "\u25c7 Response Style"),
84
+ ("tone", "\u25c7 Tone"),
85
+ ("instructions", "\u25c7 Custom Instructions"),
86
+ ("about_me", "\u25c7 About Me"),
87
+ ]),
88
+ ("MEMORY", [
89
+ ("mem_overview", "\U0001f9e0 Memory Overview"),
90
+ ("mem_saved", "\U0001f9e0 Saved Memories"),
91
+ ("mem_add", "+ Add Memory"),
92
+ ]),
93
+ ("AI BEHAVIOR", [
94
+ ("context", "\u25c7 Context"),
95
+ ("coding_prefs", "\u25c7 Coding Preferences"),
96
+ ("proactivity", "\u25c7 Proactivity"),
97
+ ]),
98
+ ("SECURITY", [
99
+ ("permissions", "\U0001f510 Security & Access"),
100
+ ("perm_list", "\U0001f6e1 Permissions"),
101
+ ("privacy", "\u25c7 Privacy"),
102
+ ]),
103
+ ]
104
+ _HINTS = {
105
+ "personality": "Tab: next field \u00b7 Esc: close",
106
+ "response_style": "Tab: next \u00b7 Esc: close",
107
+ "tone": "Click tone \u00b7 Esc: close",
108
+ "instructions": "Tab: edit \u00b7 Esc: close",
109
+ "about_me": "Tab: next \u00b7 Esc: close",
110
+ "mem_overview": "Esc: close",
111
+ "mem_saved": "Search/filter \u00b7 Edit/Delete \u00b7 Esc: close",
112
+ "mem_add": "Enter: add \u00b7 Esc: close",
113
+ "context": "Toggle \u00b7 Esc: close",
114
+ "coding_prefs": "Toggle \u00b7 Esc: close",
115
+ "proactivity": "Toggle \u00b7 Esc: close",
116
+ "permissions": "Select mode \u00b7 Esc: close",
117
+ "perm_list": "Toggle \u00b7 Esc: close",
118
+ "privacy": "Esc: close",
119
+ }
120
+ _CODING_PREF_KEYS = [
121
+ ("prefer_si", "Prefer SI units", True),
122
+ ("show_steps", "Show step-by-step", True),
123
+ ("show_derivations", "Show derivations", False),
124
+ ("code_comments", "Add code comments", True),
125
+ ("verbose_errors", "Verbose error output", False),
126
+ ("compact_output", "Compact output", False),
127
+ ]
128
+ _CONTEXT_SOURCES = [
129
+ ("current_conversation", "Current conversation", True),
130
+ ("workspace_context", "Workspace context", True),
131
+ ("project_files", "Project files", True),
132
+ ("custom_instructions", "Custom instructions", True),
133
+ ("saved_memories", "Saved memories", True),
134
+ ]
135
+ _PROACTIVITY_KEYS = [
136
+ ("auto_suggest", "Auto-suggest improvements", True),
137
+ ("explain_changes", "Explain file changes", True),
138
+ ("ask_before_files", "Ask before file changes", True),
139
+ ("command_execution", "Command execution", True),
140
+ ]
141
+ _ABOUT_ME_FIELDS = [
142
+ ("name", "Name", ""),
143
+ ("role", "Role", "e.g. Software Engineer"),
144
+ ("experience", "Experience", "e.g. 5 years"),
145
+ ("interests", "Programming interests", "e.g. AI, Web, Systems"),
146
+ ("technologies", "Preferred technologies", "e.g. Python, Rust, React"),
147
+ ("context", "Additional context", ""),
148
+ ]
149
+
150
+ def _hex(role):
151
+ if theme_css is not None:
152
+ try:
153
+ return theme_css.current_hex(role)
154
+ except Exception:
155
+ pass
156
+ return "#888888"
157
+
158
+ def _fmt_ts(ts):
159
+ if not ts:
160
+ return "\u2014"
161
+ try:
162
+ return time.strftime("%Y-%m-%d %H:%M", time.localtime(ts))
163
+ except Exception:
164
+ return "\u2014"
165
+
166
+ def _safe_query(widget, selector, widget_type=None):
167
+ try:
168
+ if widget_type:
169
+ return widget.query_one(selector, widget_type)
170
+ return widget.query_one(selector)
171
+ except Exception:
172
+ return None
173
+
174
+ def _load_about_me():
175
+ if ap is not None and hasattr(ap, "load_about_me"):
176
+ try:
177
+ return ap.load_about_me()
178
+ except Exception:
179
+ pass
180
+ try:
181
+ p = os.path.join(os.path.expanduser("~"), ".cct_about_me.json")
182
+ if os.path.exists(p):
183
+ with open(p, "r", encoding="utf-8") as f:
184
+ return json.load(f)
185
+ except Exception:
186
+ pass
187
+ return {}
188
+
189
+ def _save_about_me(data):
190
+ if ap is not None and hasattr(ap, "ABOUT_ME_FILE"):
191
+ try:
192
+ with open(ap.ABOUT_ME_FILE, "w", encoding="utf-8") as f:
193
+ json.dump(data, f, indent=2)
194
+ return True
195
+ except Exception:
196
+ pass
197
+ try:
198
+ p = os.path.join(os.path.expanduser("~"), ".cct_about_me.json")
199
+ with open(p, "w", encoding="utf-8") as f:
200
+ json.dump(data, f, indent=2)
201
+ return True
202
+ except Exception:
203
+ return False
204
+
205
+ class _NavButton(Button):
206
+ def __init__(self, key, label):
207
+ self.nav_key = key
208
+ super().__init__(label, id=f"pc-nav-{key}", classes="pc-nav-btn")
209
+
210
+ class _MemoryRow(Horizontal):
211
+ def __init__(self, memory_obj):
212
+ self.mem = memory_obj
213
+ super().__init__(classes="pc-mem-row")
214
+ def compose(self):
215
+ content = (self.mem.content or "")[:48]
216
+ if len(self.mem.content or "") > 48:
217
+ content += "\u2026"
218
+ cat = (self.mem.category or "").replace("_", " ").title()[:12]
219
+ source = getattr(self.mem, "source", "user") or "user"
220
+ source_label = {"user": "User", "ai": "AI", "imported": "Import"}.get(source, source)[:6]
221
+ yield Static(cat, classes="pc-mem-cat")
222
+ yield Static(content, classes="pc-mem-content")
223
+ yield Static(source_label, classes="pc-mem-source")
224
+ yield Static(_fmt_ts(self.mem.created_at), classes="pc-mem-date")
225
+ yield Button("\u270e", id=f"pc-mem-edit-{self.mem.id}", classes="pc-mem-edit-btn cct-btn-sm")
226
+ yield Button("\u2715", id=f"pc-mem-del-{self.mem.id}", classes="pc-mem-del-btn cct-btn-sm")
227
+
228
+ class PersonalizeCenter(Screen):
229
+ CSS = """
230
+ PersonalizeCenter { align: center middle; background: $background 70%; }
231
+ #pc-box {
232
+ width: 98; max-width: 98%; height: 40; max-height: 92%;
233
+ background: $surface; border: heavy; border-top: heavy $surface-highlight; border-left: heavy $surface-highlight; border-bottom: heavy $surface-shadow; border-right: heavy $surface-shadow; padding: 0;
234
+ layout: vertical; overflow: hidden;
235
+ }
236
+ #pc-titlebar { height: 3; min-height: 3; padding: 0 2; border-bottom: solid $border; }
237
+ #pc-title { text-style: bold; width: 1fr; }
238
+ #pc-close { min-width: 3; width: 3; height: 1; }
239
+ #pc-disabled { height: auto; padding: 2 2; align: center middle; }
240
+ .pc-disabled-msg { color: $warning; text-style: bold; height: auto; text-align: center; }
241
+ .pc-disabled-hint { color: $text-faint; height: auto; text-align: center; padding-top: 1; }
242
+ #pc-disabled-actions { height: auto; padding-top: 1; layout: horizontal; align: center middle; }
243
+ #pc-disabled-actions Button { margin: 0 1; }
244
+ #pc-body { height: 1fr; min-height: 0; layout: horizontal; }
245
+ #pc-sidebar { width: 28; min-width: 28; height: 100%; padding: 0; border-right: solid $border; }
246
+ #pc-search { height: 3; min-height: 3; padding: 0 1; border-bottom: solid $border; align-vertical: middle; }
247
+ #pc-search Input {
248
+ width: 1fr; height: 3; min-height: 3; background: $surface;
249
+ border: tall; border-top: tall $surface-shadow; border-left: tall $surface-shadow; border-bottom: tall $surface-highlight; border-right: tall $surface-highlight;
250
+ color: $text; padding: 0 1;
251
+ }
252
+ #pc-search Input:focus {
253
+ border-top: tall $accent-shadow; border-left: tall $accent-shadow; border-bottom: tall $accent-highlight; border-right: tall $accent-highlight;
254
+ }
255
+ #pc-sidebar-inner { width: 100%; height: 1fr; min-height: 0; overflow-y: auto; scrollbar-size-vertical: 1; }
256
+ .pc-nav-section { color: $text-faint; height: 1; min-height: 1; padding: 1 1 0 1; text-style: bold; }
257
+ .pc-nav-btn { width: 100%; height: 1; min-height: 1; padding: 0 1; text-align: left; background: transparent; border: none; color: $text-muted; }
258
+ .pc-nav-btn:hover { background: $accent 10%; color: $text; }
259
+ .pc-nav-btn-sel { background: $accent 15%; color: $accent; text-style: bold; }
260
+ #pc-content { width: 1fr; height: 100%; min-height: 0; padding: 1 2; overflow-y: auto; scrollbar-size-vertical: 1; }
261
+ .pc-section-title { color: $accent; height: 1; min-height: 1; padding-top: 1; margin-top: 1; text-style: bold; }
262
+ .pc-section-title:first-child { padding-top: 0; margin-top: 0; }
263
+ .pc-field { height: auto; min-height: 1; margin-bottom: 1; }
264
+ .pc-flbl { color: $text-faint; height: 1; min-height: 1; }
265
+ .pc-fval { color: $text; height: 1; min-height: 1; }
266
+ .pc-char-count { color: $text-faint; height: 1; text-align: right; }
267
+ .pc-switch-row { height: auto; min-height: 2; padding: 0 1; align-vertical: middle; border-bottom: solid $surface-alt 50%; }
268
+ .pc-switch-row:hover { background: $surface-alt 35%; }
269
+ .pc-switch-label { width: 1fr; color: $text; height: 1; }
270
+ .pc-switch-status { width: 6; color: $text-faint; height: 1; text-align: right; margin-right: 1; }
271
+ .pc-switch-row Switch, Switch {
272
+ border: none; height: 1; min-height: 1; width: 6; min-width: 6; padding: 0; margin: 0; background: transparent;
273
+ }
274
+ .pc-switch-row Switch.-on, Switch.-on { color: $success; }
275
+ .pc-switch-row Switch .switch--slider, Switch .switch--slider { color: $surface-highlight; background: $surface-shadow; }
276
+ .pc-switch-row Switch.-on .switch--slider, Switch.-on .switch--slider { color: $success; background: $surface-shadow; }
277
+ #pc-tone-row {
278
+ width: 100%; height: auto; min-height: 4; overflow-x: auto; scrollbar-size-horizontal: 1;
279
+ layout: horizontal; align-vertical: middle; padding: 0 0 1 0; margin-top: 1;
280
+ }
281
+ #pc-tone-row Button {
282
+ min-width: 12; height: 3; padding: 0 1; margin: 0 1 0 0;
283
+ background: $surface-alt; color: $text; text-style: bold;
284
+ border: heavy; border-top: heavy $surface-highlight; border-left: heavy $surface-highlight;
285
+ border-bottom: heavy $surface-shadow; border-right: heavy $surface-shadow;
286
+ transition: background 80ms, border 80ms, offset 80ms;
287
+ }
288
+ #pc-tone-row Button:hover {
289
+ background: $accent 25%; color: #ffffff;
290
+ border-top: heavy #ffffff; border-left: heavy #ffffff;
291
+ border-bottom: heavy $accent-highlight; border-right: heavy $accent-highlight;
292
+ offset-y: -1;
293
+ }
294
+ #pc-tone-row Button.-active {
295
+ offset-y: 1;
296
+ border-top: heavy $accent-shadow; border-left: heavy $accent-shadow;
297
+ border-bottom: heavy #ffffff; border-right: heavy #ffffff;
298
+ }
299
+ #pc-tone-row Button.-primary, #pc-tone-row Button.variant-primary {
300
+ background: $accent 40%; color: #ffffff;
301
+ border-top: heavy #ffffff; border-left: heavy #ffffff;
302
+ border-bottom: heavy $accent-shadow; border-right: heavy $accent-shadow;
303
+ }
304
+ #pc-tone-row Button.-primary:hover, #pc-tone-row Button.variant-primary:hover {
305
+ background: $accent 60%;
306
+ border-bottom: heavy $accent-highlight; border-right: heavy $accent-highlight;
307
+ }
308
+ .pc-mem-row { height: 1; min-height: 1; padding: 0 1; border-bottom: solid $border; }
309
+ .pc-mem-cat { width: 12; color: $accent; height: 1; }
310
+ .pc-mem-content { width: 1fr; color: $text; height: 1; overflow: hidden; }
311
+ .pc-mem-source { width: 7; color: $text-faint; height: 1; text-align: center; }
312
+ .pc-mem-date { width: 11; color: $text-faint; height: 1; }
313
+ .pc-mem-edit-btn { min-width: 3; width: 3; height: 1; padding: 0; background: transparent; color: $accent; border: none; }
314
+ .pc-mem-edit-btn:hover { background: $accent 15%; }
315
+ .pc-mem-del-btn { min-width: 3; width: 3; height: 1; padding: 0; background: transparent; color: $error; border: none; }
316
+ .pc-mem-del-btn:hover { background: $error 15%; }
317
+ .pc-empty-msg { color: $text-faint; padding: 1 1; height: auto; }
318
+ .pc-perm-mode-row { height: 1; min-height: 1; padding: 1 0; }
319
+ .pc-perm-mode-row Button { margin-right: 1; }
320
+ .pc-privacy-note { color: $text-faint; padding: 1 0; height: auto; min-height: 1; margin-top: 1; }
321
+ #pc-btns { height: auto; min-height: 4; padding: 0 1; border-top: solid $border; layout: horizontal; align-vertical: middle; }
322
+ #pc-btns-left { width: auto; height: 3; layout: horizontal; align-vertical: middle; }
323
+ #pc-btns-right { width: 1fr; height: 3; layout: horizontal; align-horizontal: right; align-vertical: middle; }
324
+ #pc-btns-left Button, #pc-btns-right Button {
325
+ min-width: 8; height: 3; padding: 0 1; margin: 0 1 0 0;
326
+ background: $surface-alt; color: $text; text-style: bold;
327
+ border: heavy; border-top: heavy $surface-highlight; border-left: heavy $surface-highlight;
328
+ border-bottom: heavy $surface-shadow; border-right: heavy $surface-shadow;
329
+ transition: background 80ms, border 80ms, offset 80ms;
330
+ }
331
+ #pc-btns-left Button:hover, #pc-btns-right Button:hover {
332
+ background: $accent 25%; color: #ffffff;
333
+ border-top: heavy #ffffff; border-left: heavy #ffffff;
334
+ border-bottom: heavy $accent-highlight; border-right: heavy $accent-highlight;
335
+ offset-y: -1;
336
+ }
337
+ #pc-btns-left Button.-active, #pc-btns-right Button.-active {
338
+ offset-y: 1;
339
+ border-top: heavy $accent-shadow; border-left: heavy $accent-shadow;
340
+ border-bottom: heavy #ffffff; border-right: heavy #ffffff;
341
+ }
342
+ #pc-btns-left Button#pc-delete {
343
+ color: $error;
344
+ border: heavy; border-top: heavy #ffffff 30%; border-left: heavy #ffffff 30%;
345
+ border-bottom: heavy $error 80%; border-right: heavy $error 80%;
346
+ }
347
+ #pc-btns-left Button#pc-delete:hover {
348
+ background: $error 30%; color: #ffffff;
349
+ border-top: heavy #ffffff; border-left: heavy #ffffff;
350
+ border-bottom: heavy $error; border-right: heavy $error;
351
+ }
352
+ #pc-btns-right Button#pc-save {
353
+ background: $accent 40%; color: #ffffff;
354
+ border-top: heavy #ffffff; border-left: heavy #ffffff;
355
+ border-bottom: heavy $accent-shadow; border-right: heavy $accent-shadow;
356
+ margin-right: 1;
357
+ }
358
+ #pc-btns-right Button#pc-save:hover {
359
+ background: $accent 60%;
360
+ border-bottom: heavy $accent-highlight; border-right: heavy $accent-highlight;
361
+ }
362
+ #pc-btns-right Button#pc-cancel {
363
+ margin-right: 0;
364
+ }
365
+ #pc-hint { color: $text-faint; height: 1; min-height: 1; padding: 0 1; margin: 0 0 1 0; }
366
+ .pc-add-mem-bar {
367
+ height: auto; min-height: 3; padding: 0; margin: 1 0;
368
+ layout: horizontal; align-vertical: middle;
369
+ }
370
+ .pc-add-mem-bar Button {
371
+ min-width: 8; height: 3; padding: 0 1; margin: 0 1 0 0;
372
+ background: $surface-alt; color: $text; text-style: bold;
373
+ border: heavy; border-top: heavy $surface-highlight; border-left: heavy $surface-highlight;
374
+ border-bottom: heavy $surface-shadow; border-right: heavy $surface-shadow;
375
+ transition: background 80ms, border 80ms, offset 80ms;
376
+ }
377
+ .pc-add-mem-bar Button:hover {
378
+ background: $accent 25%; color: #ffffff;
379
+ border-top: heavy #ffffff; border-left: heavy #ffffff;
380
+ border-bottom: heavy $accent-highlight; border-right: heavy $accent-highlight;
381
+ offset-y: -1;
382
+ }
383
+ .pc-add-mem-bar Button.-primary, .pc-add-mem-bar Button.variant-primary {
384
+ background: $accent 40%; color: #ffffff;
385
+ border-top: heavy #ffffff; border-left: heavy #ffffff;
386
+ border-bottom: heavy $accent-shadow; border-right: heavy $accent-shadow;
387
+ }
388
+ #pc-mem-search-input {
389
+ width: 100%; height: 3; min-height: 3; background: $surface;
390
+ border: tall; border-top: tall $surface-shadow; border-left: tall $surface-shadow;
391
+ border-bottom: tall $surface-highlight; border-right: tall $surface-highlight;
392
+ color: $text; padding: 0 1; margin: 0 0 1 0;
393
+ }
394
+ #pc-mem-search-input:focus {
395
+ border-top: tall $accent-shadow; border-left: tall $accent-shadow;
396
+ border-bottom: tall $accent-highlight; border-right: tall $accent-highlight;
397
+ }
398
+ .pc-mode-card { height: auto; min-height: 2; padding: 0 1; border-bottom: solid $border; }
399
+ .pc-mode-label { color: $text; height: 1; text-style: bold; }
400
+ .pc-mode-desc { color: $text-faint; height: 1; }
401
+ """
402
+
403
+ BINDINGS = [
404
+ Binding("escape", "cancel", "Cancel"),
405
+ Binding("down", "scroll_down", "Scroll down"),
406
+ Binding("up", "scroll_up", "Scroll up"),
407
+ Binding("page_down", "page_down", "Page down"),
408
+ Binding("page_up", "page_up", "Page up"),
409
+ ]
410
+
411
+ def __init__(self):
412
+ super().__init__()
413
+ self._nav_state = "personality"
414
+ self._search_query = ""
415
+ self._profiles = []
416
+ self._active_name = ""
417
+ self._selected_profile = 0
418
+ self._perm_snapshot = []
419
+ self._mem_v2_manager = None
420
+ self._mem_legacy_data = {}
421
+ self._mem_filter = "all"
422
+ self._mem_v2_all = []
423
+ self._mem_search_query = ""
424
+ self._config = None
425
+ self._coding_prefs = {}
426
+ self._about_me = {}
427
+ self._new_mem_text = ""
428
+ self._editing_mem_id = None
429
+ self._editing_mem_text = ""
430
+
431
+ def _load_data(self):
432
+ try:
433
+ if ap is not None:
434
+ self._profiles = ap.profiles()
435
+ loaded = ap.load_profiles()
436
+ self._active_name = loaded.get("active", "")
437
+ else:
438
+ self._profiles = []
439
+ self._active_name = ""
440
+ except Exception:
441
+ self._profiles = []
442
+ self._active_name = ""
443
+ try:
444
+ if perm is not None:
445
+ self._perm_snapshot = perm.manager.snapshot()
446
+ else:
447
+ self._perm_snapshot = []
448
+ except Exception:
449
+ self._perm_snapshot = []
450
+ try:
451
+ if memory is not None:
452
+ self._mem_legacy_data = memory.load()
453
+ else:
454
+ self._mem_legacy_data = {}
455
+ except Exception:
456
+ self._mem_legacy_data = {}
457
+ try:
458
+ if memory_v2 is not None:
459
+ self._mem_v2_manager = memory_v2.get_manager()
460
+ self._mem_v2_all = self._mem_v2_manager.get_all_memories()
461
+ else:
462
+ self._mem_v2_manager = None
463
+ self._mem_v2_all = []
464
+ except Exception:
465
+ self._mem_v2_manager = None
466
+ self._mem_v2_all = []
467
+ try:
468
+ if cct_config is not None:
469
+ self._config = cct_config.get_config()
470
+ else:
471
+ self._config = None
472
+ except Exception:
473
+ self._config = None
474
+ try:
475
+ self._about_me = _load_about_me()
476
+ except Exception:
477
+ self._about_me = {}
478
+ self._coding_prefs = {"prefer_si": True, "show_steps": True, "show_derivations": False, "code_comments": True, "verbose_errors": False, "compact_output": False}
479
+
480
+ def _current_profile(self):
481
+ if self._profiles and 0 <= self._selected_profile < len(self._profiles):
482
+ return self._profiles[self._selected_profile]
483
+ return None
484
+
485
+ def compose(self):
486
+ # Extension guard: Personalize is an installable extension
487
+ try:
488
+ from .. import extensions as _ext
489
+ if not _ext.is_enabled("personalize"):
490
+ with Vertical(id="pc-box"):
491
+ with Horizontal(id="pc-titlebar"):
492
+ yield Static("🎨 Personalize", id="pc-title")
493
+ yield Button("\u2715", id="pc-close", classes="cct-popup-close")
494
+ with Vertical(id="pc-disabled"):
495
+ yield Static("🎨 Personalize is currently disabled.", classes="pc-disabled-msg")
496
+ yield Static("Enable it in Extensions (main menu → Extensions) to customize AI.", classes="pc-disabled-hint")
497
+ with Horizontal(id="pc-disabled-actions"):
498
+ yield Button("Open Extensions", id="pc-open-ext", variant="primary")
499
+ yield Button("Close", id="pc-close2")
500
+ return
501
+ except Exception:
502
+ pass
503
+ with Vertical(id="pc-box"):
504
+ with Horizontal(id="pc-titlebar"):
505
+ yield Static("🎨 Personalize", id="pc-title")
506
+ yield Button("\u2715", id="pc-close", classes="cct-popup-close")
507
+ with Horizontal(id="pc-body"):
508
+ with Vertical(id="pc-sidebar"):
509
+ with Horizontal(id="pc-search"):
510
+ yield Input(placeholder="\U0001f50d Search settings\u2026", id="pc-search-input")
511
+ yield ScrollableContainer(id="pc-sidebar-inner")
512
+ with ScrollableContainer(id="pc-content"):
513
+ pass
514
+ with Horizontal(id="pc-btns"):
515
+ with Horizontal(id="pc-btns-left"):
516
+ yield Button("+ New", id="pc-new", classes="cct-btn cct-btn-sm")
517
+ yield Button("Duplicate", id="pc-dup", classes="cct-btn cct-btn-sm")
518
+ yield Button("Set Active", id="pc-active", classes="cct-btn cct-btn-sm")
519
+ yield Button("Delete", id="pc-delete", variant="error", classes="cct-btn cct-btn-sm")
520
+ with Horizontal(id="pc-btns-right"):
521
+ yield Button("Save", id="pc-save", variant="primary", classes="cct-btn cct-btn-sm")
522
+ yield Button("Cancel", id="pc-cancel", classes="cct-btn cct-btn-sm")
523
+ yield Static("", id="pc-hint")
524
+
525
+ def on_mount(self):
526
+ self._load_data()
527
+ self._rebuild_nav()
528
+ self._render_section_content()
529
+
530
+ def on_key(self, event):
531
+ if event.key == "escape":
532
+ self.action_cancel()
533
+ def action_cancel(self):
534
+ self.dismiss(None)
535
+ def action_scroll_down(self):
536
+ try:
537
+ c = self.query_one("#pc-content", ScrollableContainer)
538
+ if c.max_scroll_y > 0:
539
+ c.scroll_down(animate=False)
540
+ except Exception:
541
+ pass
542
+ def action_scroll_up(self):
543
+ try:
544
+ self.query_one("#pc-content", ScrollableContainer).scroll_up(animate=False)
545
+ except Exception:
546
+ pass
547
+ def action_page_down(self):
548
+ try:
549
+ self.query_one("#pc-content", ScrollableContainer).scroll_page_down(animate=False)
550
+ except Exception:
551
+ pass
552
+ def action_page_up(self):
553
+ try:
554
+ self.query_one("#pc-content", ScrollableContainer).scroll_page_up(animate=False)
555
+ except Exception:
556
+ pass
557
+
558
+ def _rebuild_nav(self):
559
+ try:
560
+ nav = self.query_one("#pc-sidebar-inner", ScrollableContainer)
561
+ nav.remove_children()
562
+ except Exception:
563
+ return
564
+ for section_name, items in _NAV_SECTIONS:
565
+ sec_slug = section_name.lower().replace(" ", "-")
566
+ try:
567
+ nav.mount(Static(section_name, id=f"pc-sec-{sec_slug}", classes="pc-nav-section"))
568
+ except Exception:
569
+ pass
570
+ for key, label in items:
571
+ btn = _NavButton(key, label)
572
+ if key == self._nav_state:
573
+ btn.add_class("pc-nav-btn-sel")
574
+ try:
575
+ nav.mount(btn)
576
+ except Exception:
577
+ pass
578
+ try:
579
+ self.query_one("#pc-hint", Static).update(_HINTS.get(self._nav_state, "Esc: close"))
580
+ except Exception:
581
+ pass
582
+ if getattr(self, "_search_query", ""):
583
+ self._filter_nav()
584
+
585
+ def _filter_nav(self):
586
+ query = (getattr(self, "_search_query", "") or "").strip().lower()
587
+ try:
588
+ nav = self.query_one("#pc-sidebar-inner", ScrollableContainer)
589
+ except Exception:
590
+ return
591
+ for section_name, items in _NAV_SECTIONS:
592
+ sec_slug = section_name.lower().replace(" ", "-")
593
+ sec_match = False
594
+ for key, label in items:
595
+ btn = _safe_query(nav, f"#pc-nav-{key}")
596
+ if btn is not None:
597
+ matched = not query or (query in key.lower() or query in label.lower())
598
+ btn.display = matched
599
+ if matched:
600
+ sec_match = True
601
+ hdr = _safe_query(nav, f"#pc-sec-{sec_slug}")
602
+ if hdr is not None:
603
+ hdr.display = sec_match or (not query)
604
+
605
+ def _cache_current_state(self):
606
+ # Persist whatever the user typed/selected in the currently active tab into memory
607
+ try:
608
+ prof = self._current_profile()
609
+ if prof is not None:
610
+ # Identity fields
611
+ for fid, pkey in [("pc-name", "name"), ("pc-desc", "description"), ("pc-model", "model")]:
612
+ w = _safe_query(self, f"#{fid}", Input)
613
+ if w is not None:
614
+ prof[pkey] = w.value
615
+ for fid, pkey, lo, hi in [("pc-temp", "temperature", 0, 2), ("pc-topp", "top_p", 0, 1), ("pc-creativity", "creativity", 0, 1)]:
616
+ w = _safe_query(self, f"#{fid}", Input)
617
+ if w is not None:
618
+ s = w.value.strip()
619
+ if s:
620
+ try:
621
+ prof[pkey] = max(lo, min(hi, float(s)))
622
+ except Exception:
623
+ pass
624
+ else:
625
+ prof[pkey] = None
626
+ # Tone
627
+ if _safe_query(self, "#pc-tone-balanced") is not None:
628
+ t = self._tone_from_buttons()
629
+ if t:
630
+ prof["tone"] = t
631
+ # Response Style & Segments
632
+ if _safe_query(self, "#pc-reasoning-medium") is not None:
633
+ prof["reasoning"] = self._seg_value("pc-reasoning")
634
+ if _safe_query(self, "#pc-length-medium") is not None:
635
+ prof["response_length"] = self._seg_value("pc-length")
636
+ if _safe_query(self, "#pc-teaching-direct") is not None:
637
+ prof["teaching_style"] = self._seg_value("pc-teaching")
638
+ if _safe_query(self, "#pc-memory-auto") is not None:
639
+ prof["memory_pref"] = self._seg_value("pc-memory")
640
+ # custom instructions - check both Input and TextArea
641
+ ta = _safe_query(self, "#pc-additions-ta", TextArea) if _HAS_TEXTAREA else None
642
+ if ta is not None:
643
+ try:
644
+ prof["additions"] = ta.text
645
+ except Exception:
646
+ pass
647
+ else:
648
+ w = _safe_query(self, "#pc-additions", Input)
649
+ if w is not None:
650
+ prof["additions"] = w.value
651
+ # about_me fields
652
+ for fkey, _, _ in _ABOUT_ME_FIELDS:
653
+ w = _safe_query(self, f"#pc-about-{fkey}", Input)
654
+ if w is not None:
655
+ self._about_me[fkey] = w.value.strip()
656
+ # new memory TextArea
657
+ if _HAS_TEXTAREA:
658
+ ta2 = _safe_query(self, "#pc-mem-add-ta", TextArea)
659
+ if ta2 is not None:
660
+ try:
661
+ self._new_mem_text = ta2.text
662
+ except Exception:
663
+ pass
664
+ else:
665
+ w = _safe_query(self, "#pc-mem-add-input", Input)
666
+ if w is not None:
667
+ self._new_mem_text = w.value
668
+ w = _safe_query(self, "#pc-mem-edit-input", Input)
669
+ if w is not None:
670
+ self._editing_mem_text = w.value
671
+ except Exception:
672
+ pass
673
+
674
+ def _set_nav(self, key):
675
+ try:
676
+ self._cache_current_state()
677
+ except Exception:
678
+ pass
679
+ self._nav_state = key
680
+ try:
681
+ for btn in self.query(".pc-nav-btn"):
682
+ if isinstance(btn, _NavButton):
683
+ btn.set_class(btn.nav_key == key, "pc-nav-btn-sel")
684
+ self.query_one("#pc-hint", Static).update(_HINTS.get(self._nav_state, "Esc: close"))
685
+ except Exception:
686
+ pass
687
+ self._render_section_content()
688
+
689
+ def _render_section_content(self):
690
+ try:
691
+ content = self.query_one("#pc-content", ScrollableContainer)
692
+ content.remove_children()
693
+ except Exception:
694
+ return
695
+ handler = getattr(self, f"_render_{self._nav_state}", None)
696
+ if handler:
697
+ try:
698
+ handler(content)
699
+ except Exception as e:
700
+ try:
701
+ content.mount(Static(f"Error: {e}", classes="pc-empty-msg"))
702
+ except Exception:
703
+ pass
704
+ else:
705
+ try:
706
+ content.mount(Static("Section not available.", classes="pc-empty-msg"))
707
+ except Exception:
708
+ pass
709
+
710
+ # helpers to build rows without buggy `with` context
711
+ def _field_row(self, container, label, widget):
712
+ container.mount(Static(label, classes="pc-flbl"))
713
+ container.mount(widget)
714
+
715
+ def _seg_value(self, row_id):
716
+ try:
717
+ for key, _lbl in _SEG_BTNS[row_id]:
718
+ btn = self.query_one(f"#{row_id}-{key}", Button)
719
+ if btn.variant == "primary":
720
+ return key
721
+ except Exception:
722
+ pass
723
+ return _SEG_BTNS[row_id][0][0] if _SEG_BTNS.get(row_id) else ""
724
+
725
+ def _tone_from_buttons(self):
726
+ for btone, _ in _TONE_BTNS:
727
+ try:
728
+ btn = self.query_one(f"#pc-tone-{btone}", Button)
729
+ if btn.variant == "primary":
730
+ return btone
731
+ except Exception:
732
+ continue
733
+ return "balanced"
734
+
735
+ def _unique_name(self, root):
736
+ names = {p.get("name") for p in self._profiles}
737
+ if root not in names:
738
+ return root
739
+ i = 2
740
+ while f"{root} {i}" in names:
741
+ i += 1
742
+ return f"{root} {i}"
743
+
744
+ # ── AI Personality ──
745
+ def _render_personality(self, container):
746
+ prof = self._current_profile()
747
+ try:
748
+ container.mount(Static("AI Personality", classes="pc-section-title"))
749
+ container.mount(Static("Customize how CAT communicates.", classes="pc-fval"))
750
+ container.mount(Static("Profile Identity", classes="pc-section-title"))
751
+ for fid, flbl, ph in [("pc-name", "Name", "Profile name"), ("pc-desc", "Description", "e.g. chemistry student"), ("pc-model", "Preferred Model", "gpt-4o")]:
752
+ key = {"pc-name": "name", "pc-desc": "description", "pc-model": "model"}[fid]
753
+ val = prof.get(key, "") if prof else ""
754
+ container.mount(Static(flbl, classes="pc-flbl"))
755
+ container.mount(Input(value=val, id=fid, placeholder=ph))
756
+ container.mount(Static("Model Parameters", classes="pc-section-title"))
757
+ # single column to avoid Horizontal overflow on narrow
758
+ for fid, flbl, ph, pkey in [("pc-temp", "Temperature (0-2)", "0.7", "temperature"), ("pc-topp", "Top P (0-1)", "0.9", "top_p"), ("pc-creativity", "Creativity (0-1)", "0.7", "creativity")]:
759
+ v = prof.get(pkey) if prof else None
760
+ s = "" if v is None else str(v)
761
+ container.mount(Static(flbl, classes="pc-flbl"))
762
+ container.mount(Input(value=s, id=fid, placeholder=ph))
763
+ except Exception:
764
+ pass
765
+
766
+ def _render_response_style(self, container):
767
+ prof = self._current_profile()
768
+ try:
769
+ container.mount(Static("Response Style", classes="pc-section-title"))
770
+ mapping = {"pc-reasoning": ("reasoning", "medium"), "pc-length": ("response_length", "medium"), "pc-teaching": ("teaching_style", "direct")}
771
+ labels = {"pc-reasoning": "Reasoning Depth", "pc-length": "Response Length", "pc-teaching": "Teaching Style"}
772
+ for seg_id in ("pc-reasoning", "pc-length", "pc-teaching"):
773
+ pkey, default = mapping[seg_id]
774
+ cur = (prof.get(pkey) or default) if prof else default
775
+ container.mount(Static(labels[seg_id], classes="pc-flbl"))
776
+ btns = []
777
+ for key, label in _SEG_BTNS[seg_id]:
778
+ btn = Button(label, id=f"{seg_id}-{key}", classes="cct-btn cct-btn-sm")
779
+ if key == cur:
780
+ btn.variant = "primary"
781
+ btns.append(btn)
782
+ row = Horizontal(*btns, id=seg_id)
783
+ container.mount(row)
784
+ except Exception:
785
+ pass
786
+
787
+ def _render_tone(self, container):
788
+ prof = self._current_profile()
789
+ tone = (prof.get("tone") or "balanced") if prof else "balanced"
790
+ try:
791
+ container.mount(Static("Tone", classes="pc-section-title"))
792
+ container.mount(Static("Choose CAT's voice.", classes="pc-fval"))
793
+ container.mount(Static("Communication Tone", classes="pc-flbl"))
794
+ btns = []
795
+ for btone, label in _TONE_BTNS:
796
+ btn = Button(label, id=f"pc-tone-{btone}", classes="cct-btn cct-btn-sm")
797
+ if btone == tone:
798
+ btn.variant = "primary"
799
+ btn.add_class("-primary")
800
+ btns.append(btn)
801
+ row = HorizontalScroll(*btns, id="pc-tone-row")
802
+ container.mount(row)
803
+ if ap is not None:
804
+ directive = ap.TONES.get(tone, "")
805
+ if directive:
806
+ container.mount(Static("Active Directive", classes="pc-flbl", id="pc-tone-dir-lbl"))
807
+ container.mount(Static(directive, classes="pc-fval", id="pc-tone-dir-val"))
808
+ except Exception:
809
+ pass
810
+
811
+ def _render_instructions(self, container):
812
+ prof = self._current_profile()
813
+ additions = (prof.get("additions") or "") if prof else ""
814
+ try:
815
+ container.mount(Static("Custom Instructions", classes="pc-section-title"))
816
+ container.mount(Static("Rules applied to every conversation.", classes="pc-fval"))
817
+ # Use TextArea if available for multi-line, else Input
818
+ if _HAS_TEXTAREA:
819
+ ta = TextArea(text=additions, id="pc-additions-ta")
820
+ ta.styles.height = 8
821
+ ta.styles.min_height = 4
822
+ container.mount(ta)
823
+ container.mount(Static(f"Character count: {len(additions)}", classes="pc-char-count"))
824
+ else:
825
+ container.mount(Input(value=additions, id="pc-additions", placeholder="e.g. always use SI units"))
826
+ container.mount(Static(f"Character count: {len(additions)}", classes="pc-char-count"))
827
+ except Exception:
828
+ pass
829
+
830
+ def _render_about_me(self, container):
831
+ try:
832
+ container.mount(Static("About Me", classes="pc-section-title"))
833
+ container.mount(Static("Optional — helps CAT understand you.", classes="pc-fval"))
834
+ for field_key, field_label, placeholder in _ABOUT_ME_FIELDS:
835
+ val = self._about_me.get(field_key, "")
836
+ container.mount(Static(field_label, classes="pc-flbl"))
837
+ container.mount(Input(value=val, id=f"pc-about-{field_key}", placeholder=placeholder))
838
+ except Exception:
839
+ pass
840
+
841
+ def _render_mem_overview(self, container):
842
+ try:
843
+ container.mount(Static("Memory Overview", classes="pc-section-title"))
844
+ container.mount(Static("CAT can use saved memories to personalize.", classes="pc-fval"))
845
+ v2_count = len(self._mem_v2_all)
846
+ v2_cats = {}
847
+ for m in self._mem_v2_all:
848
+ cat = m.category or "unknown"
849
+ v2_cats[cat] = v2_cats.get(cat, 0) + 1
850
+ container.mount(Static(f"Long-term memories: {v2_count}", classes="pc-flbl"))
851
+ if v2_cats:
852
+ parts = [f"{c.replace('_',' ').title()}: {n}" for c, n in sorted(v2_cats.items())]
853
+ container.mount(Static(" " + " \u00b7 ".join(parts), classes="pc-fval"))
854
+ if self._mem_legacy_data:
855
+ lt = len(self._mem_legacy_data.get("turns", []))
856
+ lf = len(self._mem_legacy_data.get("facts", []))
857
+ la = len(self._mem_legacy_data.get("activity", []))
858
+ ltop = len(self._mem_legacy_data.get("topics", {}))
859
+ container.mount(Static(f"Legacy: {lf} facts \u00b7 {lt} turns \u00b7 {la} activity \u00b7 {ltop} topics", classes="pc-fval"))
860
+ upd = self._mem_legacy_data.get("updated", 0)
861
+ if upd:
862
+ container.mount(Static(f"Last updated: {_fmt_ts(upd)}", classes="pc-fval"))
863
+ except Exception:
864
+ pass
865
+
866
+ def _render_mem_saved(self, container):
867
+ try:
868
+ container.mount(Static("Saved Memories", classes="pc-section-title"))
869
+ container.mount(Input(value=self._mem_search_query, id="pc-mem-search-input", placeholder="\U0001f50d Search memories\u2026"))
870
+ btns = []
871
+ for fkey, flabel in [("all","All"),("user","User"),("ai","AI"),("imported","Import")]:
872
+ btn = Button(flabel, id=f"pc-mem-filter-{fkey}", classes="cct-btn cct-btn-sm")
873
+ if fkey == self._mem_filter:
874
+ btn.variant = "primary"
875
+ btns.append(btn)
876
+ container.mount(Horizontal(*btns, classes="pc-add-mem-bar"))
877
+ except Exception:
878
+ pass
879
+ if self._editing_mem_id:
880
+ try:
881
+ container.mount(Static("Edit Memory", classes="pc-section-title"))
882
+ container.mount(Input(value=self._editing_mem_text, id="pc-mem-edit-input", placeholder="Edit memory\u2026"))
883
+ container.mount(Horizontal(
884
+ Button(" Save ", id="pc-mem-edit-save", variant="primary", classes="cct-btn cct-btn-sm"),
885
+ Button(" Cancel ", id="pc-mem-edit-cancel", classes="cct-btn cct-btn-sm"),
886
+ classes="pc-add-mem-bar"
887
+ ))
888
+ except Exception:
889
+ pass
890
+ # List container — rows live here so search updates don't destroy the search Input
891
+ list_box = Vertical(id="pc-mem-list")
892
+ container.mount(list_box)
893
+ try:
894
+ self._refresh_mem_list_box(list_box)
895
+ except Exception:
896
+ pass
897
+
898
+ def _refresh_mem_list_box(self, list_box=None):
899
+ try:
900
+ if list_box is None:
901
+ list_box = self.query_one("#pc-mem-list", Vertical)
902
+ list_box.remove_children()
903
+ except Exception:
904
+ return
905
+ memories_to_show = self._mem_v2_all
906
+ if self._mem_filter != "all":
907
+ memories_to_show = [m for m in memories_to_show if (getattr(m, "source", "user") or "user").lower() == self._mem_filter]
908
+ if self._mem_search_query:
909
+ q = self._mem_search_query.lower()
910
+ memories_to_show = [m for m in memories_to_show if q in (m.content or "").lower() or q in (m.category or "").lower()]
911
+ if not memories_to_show:
912
+ try:
913
+ list_box.mount(Static("No saved memories. Add one from sidebar.", classes="pc-empty-msg"))
914
+ except Exception:
915
+ pass
916
+ return
917
+ try:
918
+ list_box.mount(Static(f"Memories ({len(memories_to_show)} of {len(self._mem_v2_all)})", classes="pc-section-title"))
919
+ except Exception:
920
+ pass
921
+ for mem in memories_to_show[:25]:
922
+ try:
923
+ list_box.mount(_MemoryRow(mem))
924
+ except Exception:
925
+ pass
926
+
927
+ def _render_mem_add(self, container):
928
+ try:
929
+ container.mount(Static("Add Memory", classes="pc-section-title"))
930
+ container.mount(Static("What should CAT remember?", classes="pc-fval"))
931
+ if _HAS_TEXTAREA:
932
+ ta = TextArea(text=self._new_mem_text, id="pc-mem-add-ta")
933
+ ta.styles.height = 6
934
+ ta.styles.min_height = 4
935
+ container.mount(ta)
936
+ else:
937
+ container.mount(Input(value=self._new_mem_text, id="pc-mem-add-input", placeholder="Type a memory\u2026"))
938
+ container.mount(Horizontal(
939
+ Button(" Add Memory ", id="pc-mem-add-btn", variant="primary", classes="cct-btn cct-btn-sm"),
940
+ Button(" Cancel ", id="pc-mem-add-cancel", classes="cct-btn cct-btn-sm"),
941
+ classes="pc-add-mem-bar"
942
+ ))
943
+ except Exception:
944
+ pass
945
+
946
+ def _render_context(self, container):
947
+ try:
948
+ container.mount(Static("AI Context", classes="pc-section-title"))
949
+ container.mount(Static("What CAT can use while responding.", classes="pc-fval"))
950
+ for key, label, default in _CONTEXT_SOURCES:
951
+ status_color = _hex("success") if default else _hex("text-faint")
952
+ mark = "\u2713" if default else "\u2717"
953
+ container.mount(Horizontal(
954
+ Static(f"[{status_color}]{mark}[/] {label}", classes="pc-switch-label"),
955
+ Switch(value=default, id=f"pc-ctx-{key}"),
956
+ classes="pc-switch-row"
957
+ ))
958
+ container.mount(Static("Memory Injection", classes="pc-section-title"))
959
+ prof = self._current_profile()
960
+ mem_pref = (prof.get("memory_pref") or "auto") if prof else "auto"
961
+ btns = []
962
+ for key, label in _SEG_BTNS["pc-memory"]:
963
+ btn = Button(label, id=f"pc-memory-{key}", classes="cct-btn cct-btn-sm")
964
+ if key == mem_pref:
965
+ btn.variant = "primary"
966
+ btns.append(btn)
967
+ container.mount(Horizontal(*btns, id="pc-memory"))
968
+ turn_count = len(self._mem_legacy_data.get("turns", [])) if self._mem_legacy_data else 0
969
+ container.mount(Static(f"{turn_count} recent turns in legacy memory", classes="pc-fval"))
970
+ if self._mem_v2_manager:
971
+ try:
972
+ sess = self._mem_v2_manager.get_session_context(max_turns=20)
973
+ container.mount(Static(f"{len(sess)} turns in session memory", classes="pc-fval"))
974
+ except Exception:
975
+ pass
976
+ except Exception:
977
+ pass
978
+
979
+ def _render_coding_prefs(self, container):
980
+ try:
981
+ container.mount(Static("Coding Preferences", classes="pc-section-title"))
982
+ for key, label, default in _CODING_PREF_KEYS:
983
+ val = self._coding_prefs.get(key, default)
984
+ container.mount(Horizontal(
985
+ Static(label, classes="pc-switch-label"),
986
+ Static("ON" if val else "OFF", classes="pc-switch-status"),
987
+ Switch(value=val, id=f"pc-coding-{key}"),
988
+ classes="pc-switch-row"
989
+ ))
990
+ except Exception:
991
+ pass
992
+
993
+ def _render_proactivity(self, container):
994
+ try:
995
+ container.mount(Static("Proactivity", classes="pc-section-title"))
996
+ for key, label, default in _PROACTIVITY_KEYS:
997
+ container.mount(Horizontal(
998
+ Static(label, classes="pc-switch-label"),
999
+ Static("ON" if default else "OFF", classes="pc-switch-status"),
1000
+ Switch(value=default, id=f"pc-proact-{key}"),
1001
+ classes="pc-switch-row"
1002
+ ))
1003
+ except Exception:
1004
+ pass
1005
+
1006
+ def _render_permissions(self, container):
1007
+ current_mode = perm.manager.mode if perm is not None else "ask"
1008
+ mode_info = {
1009
+ "ask": ("\U0001f510", "Ask Every Time", "CAT asks before protected actions."),
1010
+ "restricted": ("\U0001f6e1", "Restricted", "Analyze and suggest, but blocked."),
1011
+ "full": ("\u26a1", "Full Access", "Work automatically within workspace."),
1012
+ }
1013
+ try:
1014
+ container.mount(Static("Security & Access", classes="pc-section-title"))
1015
+ container.mount(Static("Choose how CAT handles protected actions.", classes="pc-fval"))
1016
+ for mode_key in ("ask", "restricted", "full"):
1017
+ icon, label, desc = mode_info[mode_key]
1018
+ is_active = mode_key == current_mode
1019
+ mark = "\u25cf" if is_active else "\u25cb"
1020
+ container.mount(Horizontal(
1021
+ Static(f"{icon} {mark} {label}", classes="pc-mode-label"),
1022
+ classes="pc-mode-card"
1023
+ ))
1024
+ container.mount(Static(f" {desc}", classes="pc-mode-desc"))
1025
+ btns = []
1026
+ for mode_key in ("ask", "restricted", "full"):
1027
+ ml = perm.MODE_LABELS[mode_key] if perm and mode_key in perm.MODE_LABELS else mode_key.title()
1028
+ btn = Button(ml, id=f"pc-mode-{mode_key}", classes="cct-btn cct-btn-sm")
1029
+ if mode_key == current_mode:
1030
+ btn.variant = "primary"
1031
+ btns.append(btn)
1032
+ container.mount(Horizontal(*btns, classes="pc-perm-mode-row"))
1033
+ ws_path = self._config.workspace_directory if self._config and hasattr(self._config, 'workspace_directory') else "No workspace open"
1034
+ container.mount(Static("Workspace Scope", classes="pc-section-title"))
1035
+ container.mount(Static(f" {ws_path}", classes="pc-fval"))
1036
+ container.mount(Static(" Scope: current CAT workspace — no OS-wide access.", classes="pc-fval"))
1037
+ container.mount(Static("Recent Access Decisions", classes="pc-section-title"))
1038
+ log = perm.manager.log[-5:] if perm and hasattr(perm.manager, "log") else []
1039
+ if not log:
1040
+ container.mount(Static(" No recent decisions.", classes="pc-fval"))
1041
+ else:
1042
+ for ts, key, action, decision in reversed(log):
1043
+ try:
1044
+ tstr = time.strftime("%H:%M", time.localtime(ts))
1045
+ mark = "\u2713" if decision in ("allow_once","always_allow","allow") else "\u2717"
1046
+ container.mount(Static(f" {mark} {action or key} — {decision} {tstr}", classes="pc-fval"))
1047
+ except Exception:
1048
+ pass
1049
+ except Exception:
1050
+ pass
1051
+
1052
+ def _render_perm_list(self, container):
1053
+ try:
1054
+ container.mount(Static("Permissions", classes="pc-section-title"))
1055
+ container.mount(Static("Toggles overridden by access mode.", classes="pc-fval"))
1056
+ for key, label, is_on in self._perm_snapshot:
1057
+ status_color = _hex("success") if is_on else _hex("error")
1058
+ mark = "\u2713" if is_on else "\u2717"
1059
+ container.mount(Horizontal(
1060
+ Static(f"[{status_color}]{mark}[/] {label}", classes="pc-switch-label"),
1061
+ Switch(value=is_on, id=f"pc-perm-{key}"),
1062
+ classes="pc-switch-row"
1063
+ ))
1064
+ except Exception:
1065
+ pass
1066
+
1067
+ def _render_privacy(self, container):
1068
+ try:
1069
+ container.mount(Static("Privacy", classes="pc-section-title"))
1070
+ container.mount(Static("What CAT stores and where.", classes="pc-fval"))
1071
+ parts = []
1072
+ try:
1073
+ parts.append(f"Profiles: {len(self._profiles)}")
1074
+ parts.append(f"Memories: {len(self._mem_v2_all)}")
1075
+ if self._mem_legacy_data:
1076
+ parts.append(f"Legacy: {len(self._mem_legacy_data.get('facts',[]))} facts")
1077
+ except Exception:
1078
+ pass
1079
+ if parts:
1080
+ container.mount(Static(" " + " \u00b7 ".join(parts), classes="pc-fval"))
1081
+ container.mount(Static("Data Locations", classes="pc-section-title"))
1082
+ container.mount(Static(" Profiles: ~/.cct_profiles.json", classes="pc-fval"))
1083
+ container.mount(Static(" About Me: ~/.cct_about_me.json", classes="pc-fval"))
1084
+ container.mount(Static(" Legacy: ~/.cct_memory.json", classes="pc-fval"))
1085
+ container.mount(Static(" V2: ~/.cct_memory_v2/", classes="pc-fval"))
1086
+ container.mount(Static("Privacy Notes", classes="pc-privacy-note"))
1087
+ container.mount(Static(" \u2022 API keys redacted before storage.", classes="pc-fval"))
1088
+ container.mount(Static(" \u2022 Data stays on your machine.", classes="pc-fval"))
1089
+ container.mount(Static(" \u2022 Session cleared on restart.", classes="pc-fval"))
1090
+ except Exception:
1091
+ pass
1092
+
1093
+ def _save_form_to_profile(self):
1094
+ self._cache_current_state()
1095
+ prof = self._current_profile()
1096
+ if not prof:
1097
+ return False
1098
+ name = (prof.get("name") or "").strip()
1099
+ if not name:
1100
+ return False
1101
+ if ap is not None:
1102
+ try:
1103
+ ap.add_profile(
1104
+ name,
1105
+ tone=prof.get("tone", "balanced"),
1106
+ additions=prof.get("additions", ""),
1107
+ temperature=prof.get("temperature"),
1108
+ model=prof.get("model", ""),
1109
+ description=prof.get("description", ""),
1110
+ top_p=prof.get("top_p"),
1111
+ creativity=prof.get("creativity"),
1112
+ reasoning=prof.get("reasoning", "medium"),
1113
+ response_length=prof.get("response_length", "medium"),
1114
+ memory_pref=prof.get("memory_pref", "auto"),
1115
+ teaching_style=prof.get("teaching_style", "direct"),
1116
+ )
1117
+ except Exception:
1118
+ pass
1119
+ self._profiles = ap.profiles()
1120
+ return True
1121
+
1122
+ def _save_about_me(self):
1123
+ data = {}
1124
+ for field_key, _, _ in _ABOUT_ME_FIELDS:
1125
+ try:
1126
+ w = _safe_query(self, f"#pc-about-{field_key}", Input)
1127
+ if w is not None:
1128
+ data[field_key] = w.value.strip()
1129
+ elif field_key in getattr(self, "_about_me", {}):
1130
+ data[field_key] = self._about_me[field_key]
1131
+ except Exception:
1132
+ pass
1133
+ _save_about_me(data)
1134
+ self._about_me = data
1135
+
1136
+ def _save_all(self):
1137
+ self._cache_current_state()
1138
+ try:
1139
+ self._save_form_to_profile()
1140
+ except Exception:
1141
+ pass
1142
+ try:
1143
+ self._save_about_me()
1144
+ except Exception:
1145
+ pass
1146
+ try:
1147
+ for key, _lbl, _ in self._perm_snapshot:
1148
+ sw = _safe_query(self, f"#pc-perm-{key}", Switch)
1149
+ if sw is not None and perm is not None:
1150
+ perm.manager.set(key, sw.value)
1151
+ except Exception:
1152
+ pass
1153
+ try:
1154
+ for key, _lbl, _def in _CODING_PREF_KEYS:
1155
+ sw = _safe_query(self, f"#pc-coding-{key}", Switch)
1156
+ if sw is not None:
1157
+ self._coding_prefs[key] = sw.value
1158
+ except Exception:
1159
+ pass
1160
+
1161
+ def _save_and_close(self):
1162
+ self._save_all()
1163
+ try:
1164
+ self.app._system_note("\u2713 Changes saved")
1165
+ except Exception:
1166
+ pass
1167
+ self.dismiss(True)
1168
+
1169
+ def _delete_selected_profile(self):
1170
+ prof = self._current_profile()
1171
+ if not prof or ap is None:
1172
+ return
1173
+ name = prof.get("name", "")
1174
+ try:
1175
+ self._save_form_to_profile()
1176
+ except Exception:
1177
+ pass
1178
+ try:
1179
+ ap.delete_profile(name)
1180
+ if self._active_name == name:
1181
+ self._active_name = ""
1182
+ ap.set_active("")
1183
+ self._profiles = ap.profiles()
1184
+ self._selected_profile = min(self._selected_profile, max(0, len(self._profiles) - 1))
1185
+ self._rebuild_nav()
1186
+ self._render_section_content()
1187
+ except Exception:
1188
+ pass
1189
+
1190
+ def _duplicate_selected_profile(self):
1191
+ prof = self._current_profile()
1192
+ if not prof or ap is None:
1193
+ return
1194
+ try:
1195
+ self._save_form_to_profile()
1196
+ except Exception:
1197
+ pass
1198
+ name = self._unique_name(prof.get("name", "Profile") + " Copy")
1199
+ try:
1200
+ ap.add_profile(name, tone=prof.get("tone", "balanced"), additions=prof.get("additions", ""), temperature=prof.get("temperature"), model=prof.get("model", ""), description=prof.get("description", ""), top_p=prof.get("top_p"), creativity=prof.get("creativity"), reasoning=prof.get("reasoning", ""), response_length=prof.get("response_length", ""), memory_pref=prof.get("memory_pref", ""), teaching_style=prof.get("teaching_style", ""))
1201
+ self._profiles = ap.profiles()
1202
+ self._selected_profile = len(self._profiles) - 1
1203
+ self._rebuild_nav()
1204
+ self._render_section_content()
1205
+ except Exception:
1206
+ pass
1207
+
1208
+ def _new_profile(self):
1209
+ if ap is None:
1210
+ return
1211
+ name = self._unique_name("New Profile")
1212
+ try:
1213
+ ap.add_profile(name, tone="balanced", additions="", temperature=None, model="")
1214
+ self._profiles = ap.profiles()
1215
+ self._selected_profile = len(self._profiles) - 1
1216
+ self._active_name = name
1217
+ try:
1218
+ ap.set_active(name)
1219
+ except Exception:
1220
+ pass
1221
+ self._rebuild_nav()
1222
+ self._render_section_content()
1223
+ except Exception:
1224
+ pass
1225
+
1226
+ def _set_active_profile(self):
1227
+ prof = self._current_profile()
1228
+ if not prof or ap is None:
1229
+ return
1230
+ name = prof.get("name", "")
1231
+ try:
1232
+ ap.set_active(name)
1233
+ self._active_name = name
1234
+ self._rebuild_nav()
1235
+ try:
1236
+ self.app._system_note(f"\u2726 '{name}' is now active.")
1237
+ except Exception:
1238
+ pass
1239
+ except Exception:
1240
+ pass
1241
+
1242
+ def _delete_memory(self, memory_id):
1243
+ if self._mem_v2_manager is None:
1244
+ return
1245
+ try:
1246
+ self._mem_v2_manager.delete_memory(memory_id)
1247
+ self._mem_v2_all = self._mem_v2_manager.get_all_memories()
1248
+ # preserve search Input focus — refresh list only if still in mem_saved
1249
+ if self._nav_state == "mem_saved":
1250
+ try:
1251
+ self._refresh_mem_list_box()
1252
+ return
1253
+ except Exception:
1254
+ pass
1255
+ self._render_section_content()
1256
+ except Exception:
1257
+ pass
1258
+
1259
+ def _add_memory(self):
1260
+ # support TextArea id as well
1261
+ if not self._new_mem_text.strip() and _HAS_TEXTAREA:
1262
+ try:
1263
+ ta = _safe_query(self, "#pc-mem-add-ta", TextArea)
1264
+ if ta is not None:
1265
+ self._new_mem_text = ta.text
1266
+ except Exception:
1267
+ pass
1268
+ if self._mem_v2_manager is None or not self._new_mem_text.strip():
1269
+ return
1270
+ try:
1271
+ self._mem_v2_manager.store_memory("custom", self._new_mem_text.strip(), importance=0.5, confidence=1.0, source="user")
1272
+ self._mem_v2_all = self._mem_v2_manager.get_all_memories()
1273
+ self._new_mem_text = ""
1274
+ self._nav_state = "mem_saved"
1275
+ self._rebuild_nav()
1276
+ self._render_section_content()
1277
+ try:
1278
+ self.app._system_note("\u2713 Memory added")
1279
+ except Exception:
1280
+ pass
1281
+ except Exception:
1282
+ pass
1283
+
1284
+ def _confirm_mode_switch(self, mode, ok):
1285
+ if not ok:
1286
+ try:
1287
+ self._render_section_content()
1288
+ except Exception:
1289
+ pass
1290
+ return
1291
+ try:
1292
+ if perm is not None:
1293
+ perm.manager.set_mode(mode)
1294
+ self._perm_snapshot = perm.manager.snapshot() if perm is not None else []
1295
+ except Exception:
1296
+ pass
1297
+ try:
1298
+ self._render_section_content()
1299
+ except Exception:
1300
+ pass
1301
+
1302
+ def _edit_memory(self, memory_id):
1303
+ if self._mem_v2_manager is None:
1304
+ return
1305
+ try:
1306
+ self._mem_v2_manager.update_memory(memory_id, content=self._editing_mem_text)
1307
+ self._mem_v2_all = self._mem_v2_manager.get_all_memories()
1308
+ self._editing_mem_id = None
1309
+ self._editing_mem_text = ""
1310
+ self._render_section_content()
1311
+ except Exception:
1312
+ pass
1313
+
1314
+ def on_button_pressed(self, event):
1315
+ eid = event.button.id or ""
1316
+ if eid in ("pc-open-ext", "pc-close2"):
1317
+ try:
1318
+ self.dismiss(None)
1319
+ from .extensions_panel import ExtensionsPanel
1320
+ self.app.push_screen(ExtensionsPanel())
1321
+ except Exception:
1322
+ self.dismiss(None)
1323
+ return
1324
+ if eid in ("pc-cancel", "pc-close"):
1325
+ self.action_cancel()
1326
+ return
1327
+ if eid == "pc-save":
1328
+ self._save_and_close()
1329
+ return
1330
+ if eid == "pc-new":
1331
+ self._new_profile()
1332
+ return
1333
+ if eid == "pc-dup":
1334
+ self._duplicate_selected_profile()
1335
+ return
1336
+ if eid == "pc-active":
1337
+ self._set_active_profile()
1338
+ return
1339
+ if eid == "pc-delete":
1340
+ self._delete_selected_profile()
1341
+ return
1342
+ if eid.startswith("pc-nav-"):
1343
+ key = eid[len("pc-nav-"):]
1344
+ self._set_nav(key)
1345
+ return
1346
+ if eid.startswith("pc-tone-"):
1347
+ tone = eid.split("-", 2)[2]
1348
+ try:
1349
+ for btone, _ in _TONE_BTNS:
1350
+ btn = self.query_one(f"#pc-tone-{btone}", Button)
1351
+ is_active = (btone == tone)
1352
+ btn.variant = "primary" if is_active else "default"
1353
+ if is_active:
1354
+ btn.add_class("-primary")
1355
+ else:
1356
+ btn.remove_class("-primary")
1357
+ if ap is not None:
1358
+ dir_val = self.query_one("#pc-tone-dir-val", Static)
1359
+ if dir_val:
1360
+ dir_val.update(ap.TONES.get(tone, ""))
1361
+ except Exception:
1362
+ pass
1363
+ prof = self._current_profile()
1364
+ if prof:
1365
+ prof["tone"] = tone
1366
+ return
1367
+ for seg_id in ("pc-reasoning", "pc-length", "pc-memory", "pc-teaching"):
1368
+ if eid.startswith(f"{seg_id}-"):
1369
+ key = eid.split("-", 2)[2]
1370
+ try:
1371
+ for k, _label in _SEG_BTNS[seg_id]:
1372
+ btn = self.query_one(f"#{seg_id}-{k}", Button)
1373
+ btn.variant = "primary" if k == key else "default"
1374
+ except Exception:
1375
+ pass
1376
+ prof = self._current_profile()
1377
+ if prof:
1378
+ prop_map = {
1379
+ "pc-reasoning": "reasoning",
1380
+ "pc-length": "response_length",
1381
+ "pc-memory": "memory_pref",
1382
+ "pc-teaching": "teaching_style"
1383
+ }
1384
+ if seg_id in prop_map:
1385
+ prof[prop_map[seg_id]] = key
1386
+ return
1387
+ if eid.startswith("pc-mode-"):
1388
+ mode = eid.split("-", 2)[2]
1389
+ if mode == "full" and perm is not None and perm.manager.mode != "full":
1390
+ try:
1391
+ from .header import FullAccessConfirmScreen
1392
+ self.app.push_screen(FullAccessConfirmScreen(), lambda ok: self._confirm_mode_switch(mode, ok))
1393
+ except Exception:
1394
+ self._confirm_mode_switch(mode, True)
1395
+ return
1396
+ self._confirm_mode_switch(mode, True)
1397
+ return
1398
+ if eid.startswith("pc-mem-del-"):
1399
+ mem_id = eid[len("pc-mem-del-"):]
1400
+ self._delete_memory(mem_id)
1401
+ return
1402
+ if eid.startswith("pc-mem-edit-"):
1403
+ mem_id = eid[len("pc-mem-edit-"):]
1404
+ self._editing_mem_id = mem_id
1405
+ for m in self._mem_v2_all:
1406
+ if m.id == mem_id:
1407
+ self._editing_mem_text = m.content or ""
1408
+ break
1409
+ self._render_section_content()
1410
+ return
1411
+ if eid.startswith("pc-mem-filter-"):
1412
+ self._mem_filter = eid[len("pc-mem-filter-"):]
1413
+ try:
1414
+ for fkey in ("all", "user", "ai", "imported"):
1415
+ btn = self.query_one(f"#pc-mem-filter-{fkey}", Button)
1416
+ is_active = (fkey == self._mem_filter)
1417
+ btn.variant = "primary" if is_active else "default"
1418
+ if is_active:
1419
+ btn.add_class("-primary")
1420
+ else:
1421
+ btn.remove_class("-primary")
1422
+ except Exception:
1423
+ pass
1424
+ try:
1425
+ # Only refresh list, not entire content, to keep search focus
1426
+ if self._nav_state == "mem_saved":
1427
+ self._refresh_mem_list_box()
1428
+ else:
1429
+ self._render_section_content()
1430
+ except Exception:
1431
+ try:
1432
+ self._render_section_content()
1433
+ except Exception:
1434
+ pass
1435
+ return
1436
+ if eid == "pc-mem-edit-save":
1437
+ if self._editing_mem_id and self._editing_mem_text.strip():
1438
+ self._edit_memory(self._editing_mem_id)
1439
+ else:
1440
+ self._editing_mem_id = None
1441
+ self._editing_mem_text = ""
1442
+ self._render_section_content()
1443
+ return
1444
+ if eid == "pc-mem-edit-cancel":
1445
+ self._editing_mem_id = None
1446
+ self._editing_mem_text = ""
1447
+ self._render_section_content()
1448
+ return
1449
+ if eid == "pc-mem-add-btn":
1450
+ self._add_memory()
1451
+ return
1452
+ if eid == "pc-mem-add-cancel":
1453
+ self._new_mem_text = ""
1454
+ self._nav_state = "mem_saved"
1455
+ self._rebuild_nav()
1456
+ self._render_section_content()
1457
+ return
1458
+
1459
+ def _on_textarea_changed(self, ta_id, text):
1460
+ if ta_id == "pc-additions-ta":
1461
+ self._additions_cache = text
1462
+ try:
1463
+ count_el = self.query_one(".pc-char-count", Static)
1464
+ count_el.update(f"Character count: {len(text)}")
1465
+ except Exception:
1466
+ pass
1467
+ elif ta_id == "pc-mem-add-ta":
1468
+ self._new_mem_text = text
1469
+
1470
+ def on_text_area_changed(self, event):
1471
+ try:
1472
+ ta = getattr(event, "text_area", None) or getattr(event, "widget", None)
1473
+ if ta is None:
1474
+ return
1475
+ tid = getattr(ta, "id", "") or ""
1476
+ txt = getattr(ta, "text", "")
1477
+ self._on_textarea_changed(tid, txt)
1478
+ except Exception:
1479
+ pass
1480
+
1481
+ def on_input_changed(self, event):
1482
+ if event.input.id == "pc-search-input":
1483
+ self._search_query = event.value
1484
+ try:
1485
+ self._filter_nav()
1486
+ except Exception:
1487
+ pass
1488
+ elif event.input.id == "pc-mem-search-input":
1489
+ self._mem_search_query = event.value
1490
+ try:
1491
+ self._refresh_mem_list_box()
1492
+ except Exception:
1493
+ try:
1494
+ self._render_section_content()
1495
+ except Exception:
1496
+ pass
1497
+ elif event.input.id == "pc-mem-edit-input":
1498
+ self._editing_mem_text = event.value
1499
+ elif event.input.id == "pc-mem-add-input":
1500
+ self._new_mem_text = event.value
1501
+ elif event.input.id == "pc-additions":
1502
+ try:
1503
+ count_el = self.query_one(".pc-char-count", Static)
1504
+ count_el.update(f"Character count: {len(event.value)}")
1505
+ except Exception:
1506
+ pass
1507
+
1508
+ def on_input_submitted(self, event):
1509
+ if event.input.id == "pc-mem-search-input":
1510
+ self._mem_search_query = event.value
1511
+ try:
1512
+ self._render_section_content()
1513
+ except Exception:
1514
+ pass
1515
+ elif event.input.id == "pc-mem-edit-input":
1516
+ if self._editing_mem_id and event.value.strip():
1517
+ self._editing_mem_text = event.value
1518
+ self._edit_memory(self._editing_mem_id)
1519
+ elif event.input.id == "pc-mem-add-input":
1520
+ if event.value.strip():
1521
+ self._add_memory()
1522
+
1523
+ def on_switch_changed(self, event):
1524
+ sw_id = event.switch.id
1525
+ if sw_id.startswith("pc-perm-"):
1526
+ key = sw_id[len("pc-perm-"):]
1527
+ try:
1528
+ if perm is not None:
1529
+ perm.manager.set(key, event.value)
1530
+ except Exception:
1531
+ pass
1532
+ elif sw_id.startswith("pc-coding-"):
1533
+ key = sw_id[len("pc-coding-"):]
1534
+ self._coding_prefs[key] = event.value
1535
+ try:
1536
+ parent = event.switch.parent
1537
+ if parent:
1538
+ for child in parent.children:
1539
+ if isinstance(child, Static) and "pc-switch-status" in (child.classes or []):
1540
+ child.update("ON" if event.value else "OFF")
1541
+ except Exception:
1542
+ pass
1543
+ elif sw_id.startswith("pc-ctx-"):
1544
+ key = sw_id[len("pc-ctx-"):]
1545
+ try:
1546
+ parent = event.switch.parent
1547
+ if parent:
1548
+ for child in parent.children:
1549
+ if isinstance(child, Static) and "pc-switch-label" in (child.classes or []):
1550
+ status_color = _hex("success") if event.value else _hex("text-faint")
1551
+ mark = "\u2713" if event.value else "\u2717"
1552
+ lbl = next((l for k, l, _ in _CONTEXT_SOURCES if k == key), key)
1553
+ child.update(f"[{status_color}]{mark}[/] {lbl}")
1554
+ except Exception:
1555
+ pass
1556
+ elif sw_id.startswith("pc-proact-"):
1557
+ key = sw_id[len("pc-proact-"):]
1558
+ try:
1559
+ parent = event.switch.parent
1560
+ if parent:
1561
+ for child in parent.children:
1562
+ if isinstance(child, Static) and "pc-switch-status" in (child.classes or []):
1563
+ child.update("ON" if event.value else "OFF")
1564
+ except Exception:
1565
+ pass
1566
+
1567
+ else:
1568
+ PersonalizeCenter = None