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,570 @@
1
+ """
2
+ CCT UI — McpServersPanel (v0.7.8.1 full redesign).
3
+
4
+ Professional management screen for the Model Context Protocol (MCP)
5
+ client in calc_terminal/mcp.py:
6
+
7
+ - Card rows list every saved server with a live connection status:
8
+ 🟢 Connected / 🔴 Offline / 🟡 Connecting, plus transport, version,
9
+ latency, tools exposed and last sync time.
10
+ - Minimal action set: one primary "+ Add Server"; a contextual row
11
+ (Edit / Connect·Disconnect / Delete) appears only when servers
12
+ exist and acts on the selected card; Save / Cancel close the page.
13
+ - Connect/Disconnect drives calc_terminal.mcp sessions in a
14
+ background thread so the UI never blocks on a hanging server.
15
+ - The Add/Edit popup is centered with a fade + slide animation and
16
+ offers HTTP / STDIO transports, URL, headers, authentication, and a
17
+ live Test Connection button.
18
+
19
+ The app routes the Main Menu's "MCP Servers" item to this screen.
20
+ """
21
+
22
+ if __name__ == "__main__":
23
+ print("This is a library file and is not meant to be run directly.")
24
+ import sys
25
+ sys.exit(1)
26
+
27
+ import threading
28
+ import time
29
+
30
+ TEXTUAL_AVAILABLE = True
31
+ try:
32
+ from textual.screen import Screen
33
+ from textual.containers import Vertical, Horizontal, ScrollableContainer
34
+ from textual.widgets import Static, Button, Input
35
+ from textual.binding import Binding
36
+ except Exception:
37
+ TEXTUAL_AVAILABLE = False
38
+
39
+ if TEXTUAL_AVAILABLE:
40
+ from .. import mcp
41
+ from . import theme_css
42
+
43
+ _STATUS_GLYPH = {
44
+ "connected": "\U0001f7e2", # 🟢
45
+ "connecting": "\U0001f7e1", # 🟡
46
+ "disconnected": "\U0001f534", # 🔴
47
+ "failed": "\U0001f534", # 🔴
48
+ }
49
+
50
+ _STATUS_LABEL = {
51
+ "connected": "Connected",
52
+ "connecting": "Connecting",
53
+ "disconnected": "Offline",
54
+ "failed": "Offline",
55
+ }
56
+
57
+ def _status_tone(status_key):
58
+ return {
59
+ "connected": "success",
60
+ "connecting": "warning",
61
+ "disconnected": "text-faint",
62
+ "failed": "error",
63
+ }.get(status_key, "text-faint")
64
+
65
+ def _last_sync_text(server):
66
+ ts = server.get("last_connected") or 0
67
+ if not ts:
68
+ return "never"
69
+ return time.strftime("%Y-%m-%d %H:%M", time.localtime(ts))
70
+
71
+
72
+ class _ServerCard(Static):
73
+ """One server card: name + live status on line 1, transport ·
74
+ version · latency · tools · last sync on line 2."""
75
+
76
+ def __init__(self, index, server, selected=False):
77
+ self.index = index
78
+ self.server = server
79
+ super().__init__("", classes="mcp-card" + (" mcp-card-sel" if selected else ""))
80
+ self._redraw()
81
+
82
+ def _redraw(self):
83
+ name = self.server.get("name") or self.server.get("id") or "?"
84
+ status_key = self.server.get("status") or "disconnected"
85
+ glyph = _STATUS_GLYPH.get(status_key, "\U0001f534")
86
+ tone = theme_css.current_hex(_status_tone(status_key))
87
+ label = _STATUS_LABEL.get(status_key, "Offline")
88
+ kind = "local" if self.server.get("kind") == "local" else "remote"
89
+ version = self.server.get("version") or ""
90
+ latency = self.server.get("latency_ms")
91
+ lat_txt = f"{latency}ms" if latency is not None else "\u2014"
92
+ tools = self.server.get("tools") or []
93
+ tools_txt = f"{len(tools)} tools"
94
+ sync_txt = _last_sync_text(self.server)
95
+ sel = " \u25b8" if "mcp-card-sel" in self.classes else ""
96
+ line1 = (f" {glyph} [{theme_css.current_hex('text')} b]{name}[/] "
97
+ f"[{tone}]{label}[/] [{theme_css.current_hex('text-faint')}]"
98
+ f"({kind})[/]{sel}")
99
+ line2 = (f" [{theme_css.current_hex('text-faint')}]"
100
+ f"transport {kind} \u00b7 v{version if version else '-'}"
101
+ f" \u00b7 {lat_txt} \u00b7 {tools_txt}"
102
+ f" \u00b7 sync {sync_txt}[/]")
103
+ self.update(f"{line1}\n{line2}")
104
+
105
+ def set_selected(self, selected):
106
+ self.set_class(selected, "mcp-card-sel")
107
+ self._redraw()
108
+
109
+
110
+ class McpServersPanel(Screen):
111
+ """Manage MCP servers: add, edit, remove, connect, disconnect,
112
+ persist."""
113
+
114
+ CSS = """
115
+ McpServersPanel { align: center middle; background: $app-background 70%; }
116
+ #mcp-box {
117
+ width: 92; max-width: 100%; height: auto; max-height: 92%;
118
+ background: $surface; border: round $border; padding: 0;
119
+ opacity: 0; offset-y: 1;
120
+ transition: opacity 150ms, offset 180ms;
121
+ }
122
+ #mcp-box.open { opacity: 1; offset-y: 0; }
123
+ #mcp-titlebar {
124
+ height: 3; padding: 1 2 0 2;
125
+ border-bottom: solid $border;
126
+ }
127
+ #mcp-title { text-style: bold; width: 1fr; }
128
+ #mcp-subtitle { color: $text-faint; height: 2; padding: 0 2; }
129
+ #mcp-list { height: auto; min-height: 4; padding: 1 2; overflow-y: auto; scrollbar-gutter: stable; }
130
+ .mcp-card {
131
+ height: 3; margin-bottom: 1;
132
+ background: $app-background; border: round $border;
133
+ padding: 0 1;
134
+ }
135
+ .mcp-card-sel { border: round $border-active; }
136
+ .mcp-empty { color: $text-faint; padding: 2 1; }
137
+ #mcp-add-row { height: 4; padding: 0 2 1 2; }
138
+ #mcp-add-row Button { margin-right: 1; }
139
+ #mcp-ctx-actions { height: 4; padding: 0 2 1 2; }
140
+ #mcp-ctx-actions Button { margin-right: 1; width: 1fr; min-width: 10; }
141
+ #mcp-actions { height: 5; padding: 0 2 1 2; border-top: solid $border; }
142
+ #mcp-actions Button { margin-right: 1; }
143
+ #mcp-box.cct-compact #mcp-subtitle { display: none; }
144
+ """
145
+
146
+ BINDINGS = [
147
+ Binding("escape", "cancel", "Cancel"),
148
+ Binding("down", "move_down_sel", "Next row"),
149
+ Binding("up", "move_up_sel", "Previous row"),
150
+ ]
151
+
152
+ def __init__(self):
153
+ super().__init__()
154
+ self._servers = mcp.load_servers()
155
+ self._selected = 0
156
+
157
+ def compose(self):
158
+ with Vertical(id="mcp-box"):
159
+ with Horizontal(id="mcp-titlebar"):
160
+ yield Static("\U0001f310 MCP Servers", id="mcp-title")
161
+ yield Button("\u2715", id="mcp-close", classes="cct-popup-close")
162
+ yield Static(
163
+ "Model Context Protocol servers used by CAT.",
164
+ id="mcp-subtitle")
165
+ with ScrollableContainer(id="mcp-list"):
166
+ yield from self._rows()
167
+ with Horizontal(id="mcp-add-row"):
168
+ yield Button("+ Add Server", id="mcp-add", variant="primary",
169
+ classes="cct-btn")
170
+ with Horizontal(id="mcp-ctx-actions"):
171
+ yield Button("Edit", id="mcp-edit", classes="cct-btn")
172
+ yield Button("Connect", id="mcp-connect", classes="cct-btn")
173
+ yield Button("Delete", id="mcp-remove", classes="cct-btn")
174
+ with Horizontal(id="mcp-actions"):
175
+ yield Button("Save", id="mcp-save", variant="primary", classes="cct-btn")
176
+ yield Button("Cancel", id="mcp-cancel", classes="cct-btn")
177
+
178
+ def _rows(self):
179
+ if not self._servers:
180
+ yield Static("No MCP servers configured.", classes="mcp-empty")
181
+ yield Static("Add an MCP server to expose its tools to CAT.",
182
+ classes="mcp-empty")
183
+ return
184
+ for i, server in enumerate(self._servers):
185
+ yield _ServerCard(i, server, selected=(i == self._selected))
186
+
187
+ def _rebuild_list(self):
188
+ try:
189
+ self.query_one("#mcp-list", ScrollableContainer).remove_children()
190
+ for row in self._rows():
191
+ self.query_one("#mcp-list", ScrollableContainer).mount(row)
192
+ self._refresh_actions()
193
+ self._fit()
194
+ except Exception:
195
+ pass
196
+
197
+ def _refresh_actions(self):
198
+ """Contextual actions row appears only when servers exist;
199
+ the Connect button flips to Disconnect for a connected
200
+ server. Only what's relevant is ever on screen."""
201
+ has = bool(self._servers)
202
+ try:
203
+ self.query_one("#mcp-ctx-actions").display = has
204
+ except Exception:
205
+ pass
206
+ if not has:
207
+ return
208
+ server = self._current()
209
+ connected = bool(server and server.get("status") == "connected")
210
+ try:
211
+ btn = self.query_one("#mcp-connect", Button)
212
+ btn.label = "Disconnect" if connected else "Connect"
213
+ btn.variant = "error" if connected else "default"
214
+ except Exception:
215
+ pass
216
+
217
+ def _fit(self):
218
+ """Re-fit after content changes: content_size is only valid
219
+ after the next layout pass, so always defer one frame."""
220
+ try:
221
+ self.call_after_refresh(
222
+ lambda: theme_css.fit_dialog(self, "mcp-box", "mcp-list"))
223
+ except Exception:
224
+ pass
225
+
226
+ def _refresh_status(self):
227
+ """Legacy no-op kept for callers outside this class."""
228
+ return
229
+
230
+ def on_mount(self):
231
+ self.call_after_refresh(lambda: self.query_one("#mcp-box").add_class("open"))
232
+ self.call_after_refresh(self._fit)
233
+ self._refresh_actions()
234
+
235
+ def on_resize(self, event):
236
+ self._fit()
237
+
238
+ def on_click(self, event):
239
+ widget = getattr(event, "widget", None)
240
+ if isinstance(widget, _ServerCard):
241
+ if self._servers and widget.index < len(self._servers):
242
+ self._selected = widget.index
243
+ self._rebuild_list()
244
+ return
245
+ if widget in (self, None):
246
+ self.dismiss(None)
247
+
248
+ def on_key(self, event):
249
+ if event.key == "escape":
250
+ self.dismiss(None)
251
+
252
+ def action_cancel(self):
253
+ self.dismiss(None)
254
+
255
+ def action_move_down_sel(self):
256
+ if self._servers:
257
+ self._selected = min(self._selected + 1, len(self._servers) - 1)
258
+ self._rebuild_list()
259
+
260
+ def action_move_up_sel(self):
261
+ if self._servers:
262
+ self._selected = max(self._selected - 1, 0)
263
+ self._rebuild_list()
264
+ def _current(self):
265
+ if not self._servers:
266
+ return None
267
+ return self._servers[min(self._selected, len(self._servers) - 1)]
268
+
269
+ def on_button_pressed(self, event):
270
+ eid = event.button.id
271
+ if eid in ("mcp-cancel", "mcp-close"):
272
+ self.dismiss(None)
273
+ elif eid == "mcp-add":
274
+ self.app.push_screen(_AddMcpForm(), self._on_saved)
275
+ elif eid == "mcp-edit":
276
+ server = self._current()
277
+ if server:
278
+ self.app.push_screen(_AddMcpForm(server), self._on_saved)
279
+ elif eid == "mcp-remove":
280
+ server = self._current()
281
+ if server:
282
+ mcp.remove_server(server.get("id", ""))
283
+ self._servers = mcp.load_servers()
284
+ self._selected = min(self._selected, max(0, len(self._servers) - 1))
285
+ self._rebuild_list()
286
+ elif eid == "mcp-connect":
287
+ self._toggle_connect()
288
+ elif eid == "mcp-save":
289
+ mcp.save_servers(self._servers)
290
+ try:
291
+ self.app._system_note(
292
+ f"\u2713 MCP servers saved \u2014 {len(self._servers)} configured.")
293
+ except Exception:
294
+ pass
295
+ self.dismiss(None)
296
+
297
+ def _toggle_connect(self):
298
+ """One contextual Connect/Disconnect action: disconnects a
299
+ connected server, connects a disconnected one."""
300
+ server = self._current()
301
+ if not server:
302
+ return
303
+ if server.get("status") == "connected":
304
+ mcp.disconnect_server(server.get("id", ""))
305
+ server["status"] = "disconnected"
306
+ server["version"] = ""
307
+ server["latency_ms"] = None
308
+ server["tools"] = []
309
+ self._rebuild_list()
310
+ return
311
+ server["status"] = "connecting"
312
+ self._rebuild_list()
313
+
314
+ def _run():
315
+ ok, msg = mcp.connect_server(server)
316
+ live = mcp.server_status(server)
317
+ server["status"] = "connected" if ok else "failed"
318
+ server["version"] = live["version"]
319
+ server["latency_ms"] = live["latency_ms"]
320
+ server["tools"] = list(live["tools"])
321
+ if ok:
322
+ server["last_connected"] = int(time.time())
323
+ self.app.call_from_thread(self._rebuild_list)
324
+ try:
325
+ if self.app._system_note:
326
+ self.app._system_note(f"MCP '{server.get('name', '?')}': {msg}")
327
+ except Exception:
328
+ pass
329
+
330
+ threading.Thread(target=_run, daemon=True).start()
331
+
332
+ def _on_saved(self, server):
333
+ if not server:
334
+ return
335
+ mcp.add_server(server)
336
+ self._servers = mcp.load_servers()
337
+ self._selected = len(self._servers) - 1
338
+ self._rebuild_list()
339
+
340
+
341
+ class _AddMcpForm(Screen):
342
+ """Add/Edit MCP server popup (v0.7.8.1 redesign): centered,
343
+ animated, equal-width buttons, HTTP / STDIO toggle, URL,
344
+ headers, authentication, and a live Test Connection button.
345
+ Pass an existing server record to edit it in place."""
346
+
347
+ CSS = """
348
+ _AddMcpForm { align: center middle; background: $app-background 70%; }
349
+ #amf-box {
350
+ width: 66; height: auto; max-height: 92%; background: $surface;
351
+ border: round $border; padding: 0;
352
+ opacity: 0; offset-y: 1;
353
+ transition: opacity 150ms, offset 180ms;
354
+ }
355
+ #amf-box.open { opacity: 1; offset-y: 0; }
356
+ #amf-titlebar { height: 3; padding: 1 2 0 2; border-bottom: solid $border; }
357
+ #amf-title { text-style: bold; width: 1fr; }
358
+ #amf-body { padding: 1 2; overflow-y: auto; scrollbar-gutter: stable; }
359
+ #amf-kind { height: 4; padding: 1 2 0 2; }
360
+ #amf-kind Button { margin-right: 1; }
361
+ #amf-status { color: $text-faint; height: 1; padding-top: 1; }
362
+ #amf-actions { height: 5; padding: 0 2 1 2; border-top: solid $border; }
363
+ #amf-actions Button { margin-right: 1; width: 1fr; min-width: 10; }
364
+ .amf-hint { color: $text-faint; padding: 0 2 1 2; }
365
+ #amf-box.cct-compact .amf-hint { display: none; }
366
+ """
367
+
368
+ BINDINGS = [Binding("escape", "cancel", "Cancel")]
369
+
370
+ def __init__(self, server=None):
371
+ super().__init__()
372
+ self._server = server or {}
373
+ self._kind = self._server.get("kind", "remote")
374
+ self._tested_ok = False
375
+
376
+ def compose(self):
377
+ with Vertical(id="amf-box"):
378
+ with Horizontal(id="amf-titlebar"):
379
+ yield Static("Add MCP Server" if not self._server else "Edit MCP Server",
380
+ id="amf-title")
381
+ yield Button("\u2715", id="amf-close", classes="cct-popup-close")
382
+ with Vertical(id="amf-body"):
383
+ yield Static("Server Name", classes="cct-field-label")
384
+ yield Input(placeholder="My Tool Server", id="amf-name",
385
+ value=self._server.get("name", ""))
386
+ yield Static("Connection Type", classes="cct-field-label")
387
+ with Horizontal(id="amf-kind"):
388
+ yield Button("HTTP", id="amf-kind-remote",
389
+ variant="primary" if self._kind == "remote" else "default",
390
+ classes="cct-btn cct-btn-sm")
391
+ yield Button("STDIO", id="amf-kind-local",
392
+ variant="primary" if self._kind == "local" else "default",
393
+ classes="cct-btn cct-btn-sm")
394
+ yield Static("Endpoint URL (HTTP only \u2014 must end in /mcp or /jsonrpc)",
395
+ classes="cct-field-label amf-remote-field")
396
+ yield Input(placeholder="http://localhost:3000/mcp", id="amf-url",
397
+ value=self._server.get("url", ""),
398
+ classes="amf-remote-field on")
399
+ yield Static("Command (STDIO only \u2014 e.g. npx -y server, argument per word)",
400
+ classes="cct-field-label amf-local-field")
401
+ yield Input(placeholder="npx -y my-mcp-server", id="amf-command",
402
+ value=self._server.get("command", ""),
403
+ classes="amf-local-field")
404
+ yield Static("Extra headers (optional, format: Key: Value)",
405
+ classes="cct-field-label amf-remote-field")
406
+ yield Input(placeholder="Authorization: Bearer abc", id="amf-header",
407
+ value=_headers_to_str(self._server.get("headers") or {}),
408
+ classes="amf-remote-field")
409
+ yield Static("Authentication token (optional \u2014 sent as a Bearer header)",
410
+ classes="cct-field-label amf-remote-field")
411
+ yield Input(placeholder="sk-...", id="amf-auth",
412
+ value=self._server.get("auth_token", ""),
413
+ password=True, classes="amf-remote-field")
414
+ yield Static("", id="amf-status")
415
+ with Horizontal(id="amf-actions"):
416
+ yield Button(" \u2699 Test Connection ", id="amf-test", classes="cct-btn")
417
+ yield Button("Add", id="amf-add", variant="primary", classes="cct-btn")
418
+ yield Button("Cancel", id="amf-cancel", classes="cct-btn")
419
+ yield Static("Enter to confirm \u00b7 Esc to cancel", classes="amf-hint")
420
+
421
+ def _apply_kind(self):
422
+ kind = self._kind
423
+ for name in ("command", "url", "header", "auth"):
424
+ try:
425
+ field = self.query_one(f"#amf-{name}", Input)
426
+ except Exception:
427
+ continue
428
+ show_field = (name == "command" and kind == "local") or \
429
+ (name in ("url", "header", "auth") and kind == "remote")
430
+ field.display = show_field
431
+ for cls in ("amf-remote-field", "amf-local-field"):
432
+ for w in self.query(f".{cls}"):
433
+ shown = (cls == "amf-local-field" and kind == "local") or \
434
+ (cls == "amf-remote-field" and kind == "remote")
435
+ w.display = shown
436
+ self.query_one("#amf-kind-remote", Button).variant = \
437
+ "primary" if kind == "remote" else "default"
438
+ self.query_one("#amf-kind-local", Button).variant = \
439
+ "primary" if kind == "local" else "default"
440
+ self._fit()
441
+
442
+ def on_mount(self):
443
+ self._apply_kind()
444
+ self.call_after_refresh(lambda: self.query_one("#amf-box").add_class("open"))
445
+ self.call_after_refresh(self._fit)
446
+ try:
447
+ self.query_one("#amf-name", Input).focus()
448
+ except Exception:
449
+ pass
450
+
451
+ def on_resize(self, event):
452
+ self._fit()
453
+
454
+ def _fit(self):
455
+ try:
456
+ self.call_after_refresh(
457
+ lambda: theme_css.fit_dialog(self, "amf-box", "amf-body"))
458
+ except Exception:
459
+ pass
460
+
461
+ def action_cancel(self):
462
+ self.dismiss(None)
463
+
464
+ def on_click(self, event):
465
+ wid = getattr(event.widget, "id", None) if getattr(event, "widget", None) else None
466
+ if wid == "amf-kind-remote":
467
+ self._kind = "remote"
468
+ self._apply_kind()
469
+ elif wid == "amf-kind-local":
470
+ self._kind = "local"
471
+ self._apply_kind()
472
+ elif getattr(event, "widget", None) in (self, None):
473
+ self.dismiss(None)
474
+
475
+ def on_key(self, event):
476
+ if event.key == "escape":
477
+ self.dismiss(None)
478
+
479
+ def on_button_pressed(self, event):
480
+ eid = event.button.id
481
+ if eid == "amf-add":
482
+ self._submit()
483
+ elif eid == "amf-cancel" or eid == "amf-close":
484
+ self.dismiss(None)
485
+ elif eid == "amf-test":
486
+ self._test_connection()
487
+ else:
488
+ self.on_click(type("e", (), {"widget": event.button})())
489
+
490
+ def on_input_submitted(self, event):
491
+ if event.input.id == "amf-name":
492
+ self._submit()
493
+
494
+ def _build_record(self):
495
+ server = dict(self._server)
496
+ name = self.query_one("#amf-name", Input).value.strip()
497
+ server["name"] = name
498
+ server["kind"] = self._kind
499
+ if self._kind == "local":
500
+ server["command"] = self.query_one("#amf-command", Input).value.strip()
501
+ else:
502
+ url = self.query_one("#amf-url", Input).value.strip()
503
+ if url:
504
+ server["url"] = url
505
+ header = self.query_one("#amf-header", Input).value.strip()
506
+ headers = {}
507
+ if header and ":" in header:
508
+ k, _, v = header.partition(":")
509
+ headers[k.strip()] = v.strip()
510
+ auth = self.query_one("#amf-auth", Input).value.strip()
511
+ if auth:
512
+ headers["Authorization"] = f"Bearer {auth}"
513
+ server["headers"] = headers
514
+ server.setdefault("enabled", True)
515
+ server.setdefault("status", "disconnected")
516
+ server.setdefault("version", "")
517
+ server.setdefault("latency_ms", None)
518
+ server.setdefault("tools", [])
519
+ return server
520
+
521
+ def _submit(self):
522
+ server = self._build_record()
523
+ if not server.get("name"):
524
+ self.query_one("#amf-status", Static).update(
525
+ f"[{theme_css.current_hex('error')}]\u2717 Name is required[/]")
526
+ return
527
+ if self._kind == "local" and not server.get("command"):
528
+ self.query_one("#amf-status", Static).update(
529
+ f"[{theme_css.current_hex('error')}]\u2717 Command is required for STDIO[/]")
530
+ return
531
+ if self._kind == "remote" and not str(server.get("url", "")).startswith("http"):
532
+ self.query_one("#amf-status", Static).update(
533
+ f"[{theme_css.current_hex('error')}]\u2717 A valid HTTP endpoint URL is required[/]")
534
+ return
535
+ self.dismiss(server)
536
+
537
+ def _test_connection(self):
538
+ server = self._build_record()
539
+ if not server.get("name"):
540
+ self.query_one("#amf-status", Static).update(
541
+ f"[{theme_css.current_hex('error')}]\u2717 Name is required before testing[/]")
542
+ return
543
+ status = self.query_one("#amf-status", Static)
544
+ status.update(
545
+ f"[{theme_css.current_hex('warning')}]\U0001f7e1 Testing connection\u2026[/]")
546
+ test_server = dict(server)
547
+ test_server["id"] = f"test-{int(time.time())}"
548
+ test_server["status"] = "connecting"
549
+
550
+ def _run():
551
+ try:
552
+ ok, msg = mcp.connect_server(test_server)
553
+ except Exception as e:
554
+ ok, msg = False, str(e)
555
+ self.app.call_from_thread(
556
+ lambda: status.update(
557
+ f"[{theme_css.current_hex('success' if ok else 'error')}]"
558
+ f"{'\u2713' if ok else '\u2717'} {msg}[/]"))
559
+
560
+ threading.Thread(target=_run, daemon=True).start()
561
+
562
+
563
+ def _headers_to_str(headers):
564
+ if not headers:
565
+ return ""
566
+ return ", ".join(f"{k}: {v}" for k, v in headers.items())
567
+
568
+ else:
569
+ McpServersPanel = None
570
+ _AddMcpForm = None