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