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,231 @@
1
+ """
2
+ CCT UI — ActivityPanel (v0.7.8.1 Live Thinking panel).
3
+
4
+ A small live popup (Ctrl+T) showing the state of the turn currently
5
+ in flight — the "live thinking" surface the spec asks for:
6
+
7
+ spinner + stage ......... live (thinking.detect_stage, same engine
8
+ as the composer's status bar)
9
+ elapsed timer ........... live
10
+ tokens streamed ......... live chunk counter from the worker
11
+ files touched ........... agent/pipeline tool steps with a path
12
+ last tool step .......... most recent agent tool (name + target)
13
+ provider / model ........ from aicore config
14
+ session usage ........... requests + total tokens (aicore)
15
+
16
+ It never pokes widget internals: CCTApp._activity is a plain dict of
17
+ counters the workers bump (see _stream_worker / _pipeline_worker),
18
+ and the stage/timer recomputation reads the composer's streaming
19
+ state through tiny defensive getters. When nothing is running it
20
+ reports "idle" honestly instead of faking activity.
21
+ """
22
+
23
+ if __name__ == "__main__":
24
+ print("This is a library file and is not meant to be run directly.")
25
+ import sys
26
+ sys.exit(1)
27
+
28
+ import time
29
+
30
+ TEXTUAL_AVAILABLE = True
31
+ try:
32
+ from textual.screen import Screen
33
+ from textual.containers import Vertical, Horizontal
34
+ from textual.widgets import Static, Button
35
+ from textual.binding import Binding
36
+ except Exception:
37
+ TEXTUAL_AVAILABLE = False
38
+
39
+ if TEXTUAL_AVAILABLE:
40
+ from . import thinking
41
+ from . import theme_css
42
+
43
+
44
+ class ActivityPanel(Screen):
45
+ """Live activity popup for the in-flight turn (Ctrl+T)."""
46
+
47
+ CSS = """
48
+ ActivityPanel { align: center middle; background: $app-background 70%; }
49
+ #act-box {
50
+ width: 62; height: auto; max-height: 90%;
51
+ background: $surface; border: round $border; padding: 0;
52
+ opacity: 0; offset-y: 1;
53
+ transition: opacity 150ms, offset 180ms;
54
+ }
55
+ #act-box.open { opacity: 1; offset-y: 0; }
56
+ #act-titlebar { height: 3; padding: 1 2 0 2; border-bottom: solid $border; }
57
+ #act-title { text-style: bold; width: 1fr; }
58
+ #act-body { padding: 1 2; overflow-y: auto; }
59
+ #act-stage { height: 1; }
60
+ #act-stats { height: 8; padding-top: 1; color: $text-muted; }
61
+ #act-usage { height: 2; padding-top: 1; color: $text-faint; }
62
+ #act-actions { height: 5; padding: 0 2 1 2; border-top: solid $border; }
63
+ #act-actions Button { margin-right: 1; }
64
+ .act-hint { color: $text-faint; padding: 0 2 1 2; }
65
+ """
66
+
67
+ BINDINGS = [Binding("escape", "cancel", "Cancel")]
68
+
69
+ def __init__(self):
70
+ super().__init__()
71
+ self._spin_tick = 0
72
+
73
+ def compose(self):
74
+ with Vertical(id="act-box"):
75
+ with Horizontal(id="act-titlebar"):
76
+ yield Static("\U0001f9e0 Live Activity", id="act-title")
77
+ yield Button("\u2715", id="act-close", classes="cct-popup-close")
78
+ with Vertical(id="act-body"):
79
+ yield Static("", id="act-stage")
80
+ yield Static("", id="act-stats")
81
+ yield Static("", id="act-usage")
82
+ with Horizontal(id="act-actions"):
83
+ yield Button("\u23f9 Stop", id="act-stop", variant="error", classes="cct-btn")
84
+ yield Button("Close", id="act-close-btn", classes="cct-btn")
85
+ yield Static("Esc to close", classes="act-hint")
86
+
87
+ def on_mount(self):
88
+ self.call_after_refresh(lambda: self.query_one("#act-box").add_class("open"))
89
+ self.call_after_refresh(self._fit)
90
+ # Cache the activity module once at mount, not every 100ms tick
91
+ try:
92
+ from .. import activity as _act_mod
93
+ self._act_mod = _act_mod
94
+ except Exception:
95
+ self._act_mod = None
96
+ self._timer = self.set_interval(0.1, self._refresh)
97
+
98
+ def on_unmount(self):
99
+ # Mirror ActivityStreamPanel: the 100ms refresh ticker is
100
+ # owned by this screen and must stop when it closes.
101
+ try:
102
+ if getattr(self, "_timer", None) is not None:
103
+ self._timer.stop()
104
+ except Exception:
105
+ pass
106
+ self._timer = None
107
+
108
+ def on_resize(self, event):
109
+ self._fit()
110
+
111
+ def _fit(self):
112
+ try:
113
+ self.call_after_refresh(
114
+ lambda: theme_css.fit_dialog(self, "act-box", "act-body"))
115
+ except Exception:
116
+ pass
117
+
118
+ def _refresh(self):
119
+ try:
120
+ self._spin_tick += 1
121
+ app = self.app
122
+ streaming = bool(getattr(app, "_is_streaming", False))
123
+ # ── REAL activity: single source of truth ──
124
+ _act = self._act_mod
125
+ if _act is not None:
126
+ tid = getattr(app, "_streaming_turn_id", None) or getattr(app.composer, "_stream_turn_id", None)
127
+ acts = _act.manager.get_for_turn(tid) if tid else []
128
+ running = [a for a in acts if a.status in (_act.RUNNING, _act.PENDING, _act.WAITING)]
129
+ target = running[-1] if running else (acts[-1] if acts else None)
130
+ if target:
131
+ # real data
132
+ elapsed = target.elapsed_time
133
+ # spinner by status
134
+ if target.status == _act.RUNNING:
135
+ spin = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"][self._spin_tick % 10]
136
+ color = theme_css.current_hex("accent")
137
+ elif target.status == _act.WAITING:
138
+ spin = "?"
139
+ color = theme_css.current_hex("warning")
140
+ elif target.status == _act.FAILED:
141
+ spin = "✕"
142
+ color = theme_css.current_hex("error")
143
+ elif target.status == _act.COMPLETED:
144
+ spin = "✓"
145
+ color = theme_css.current_hex("success")
146
+ else:
147
+ spin = "●"
148
+ color = theme_css.current_hex("text-faint")
149
+ title = target.title or target.current_step or "Working"
150
+ prov = f"{target.provider} {target.model}".strip() or f"{target.provider or ''} {target.model or ''}".strip()
151
+ if not prov:
152
+ try:
153
+ from .. import aicore as _ac
154
+ cfg = _ac.load_config()
155
+ prov = f"{(cfg.get('provider') or '').upper()} {cfg.get('model') or ''}".strip()
156
+ except Exception:
157
+ prov = "—"
158
+ stage_box = self.query_one("#act-stage", Static)
159
+ stage_box.update(f"[{color}]{spin}[/] [{color}]{title}[/] · [{color}]{target.status}[/] · {target.elapsed_time:.1f}s")
160
+ # stats from real activity
161
+ lines = [
162
+ f" [{theme_css.current_hex('text-faint')}]elapsed[/] {target.elapsed_time:.1f}s",
163
+ f" [{theme_css.current_hex('text-faint')}]status[/] {target.status}",
164
+ f" [{theme_css.current_hex('text-faint')}]provider[/] {prov or '—'}",
165
+ f" [{theme_css.current_hex('text-faint')}]progress[/] {int((target.progress or 0)*100)}%" if target.progress is not None else f" [{theme_css.current_hex('text-faint')}]progress[/] —",
166
+ f" [{theme_css.current_hex('text-faint')}]workspace[/] {target.workspace or '—'}",
167
+ ]
168
+ if target.file:
169
+ lines.append(f" [{theme_css.current_hex('text-faint')}]file[/] {target.file}")
170
+ if target.command:
171
+ lines.append(f" [{theme_css.current_hex('text-faint')}]command[/] {target.command[:40]}")
172
+ if target.error:
173
+ lines.append(f" [{theme_css.current_hex('text-faint')}]error[/] {target.error[:60]}")
174
+ stats_box = self.query_one("#act-stats", Static)
175
+ stats_box.update("\n".join(lines))
176
+ # usage
177
+ try:
178
+ from .. import aicore
179
+ usage = aicore.get_session_usage()
180
+ except Exception:
181
+ usage = {}
182
+ usage_box = self.query_one("#act-usage", Static)
183
+ if isinstance(usage, dict):
184
+ usage_box.update(
185
+ " session: "
186
+ f"{usage.get('requests', 0)} requests · "
187
+ f"{usage.get('prompt_tokens', 0)} prompt · "
188
+ f"{usage.get('completion_tokens', 0)} completion · "
189
+ f"{usage.get('total_tokens', 0)} total")
190
+ stop_btn = self.query_one("#act-stop", Button)
191
+ stop_btn.display = streaming and target.cancellable
192
+ return
193
+ # fallback: no real activity -> idle (never fake)
194
+ stage_box = self.query_one("#act-stage", Static)
195
+ stats_box = self.query_one("#act-stats", Static)
196
+ usage_box = self.query_one("#act-usage", Static)
197
+ stop_btn = self.query_one("#act-stop", Button)
198
+ stop_btn.display = streaming
199
+ if not streaming:
200
+ stage_box.update(f"[{theme_css.current_hex('text-faint')}]⎺ Idle — no turn in flight[/]")
201
+ stats_box.update(f" [{theme_css.current_hex('text-faint')}]No active tasks[/]\n start a message to watch it live.")
202
+ usage_box.update("")
203
+ else:
204
+ # streaming but no activity yet -> minimal real spinner
205
+ spin = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"][self._spin_tick % 10]
206
+ color = theme_css.current_hex("accent")
207
+ stage_box.update(f"[{color}]{spin}[/] [{color}]Working…[/] · {time.time() - (getattr(app, '_activity', {}).get('start') or time.time()):.1f}s")
208
+ stats_box.update(" preparing real activity…")
209
+ usage_box.update("")
210
+ except Exception:
211
+ pass
212
+
213
+ def action_cancel(self):
214
+ self.dismiss(None)
215
+
216
+ def on_button_pressed(self, event):
217
+ eid = event.button.id
218
+ if eid in ("act-close", "act-close-btn"):
219
+ self.dismiss(None)
220
+ elif eid == "act-stop":
221
+ try:
222
+ self.app.action_cancel_streaming()
223
+ except Exception:
224
+ pass
225
+
226
+ def on_key(self, event):
227
+ if event.key == "escape":
228
+ self.dismiss(None)
229
+
230
+ else:
231
+ ActivityPanel = None
@@ -0,0 +1,238 @@
1
+ """
2
+ CCT UI — activity_stream_panel.py: Activity Stream panel (v0.7.6).
3
+ A Textual-based embedded panel that shows real-time tool execution
4
+ activity from the event_stream. Displays a live-scrolling list of
5
+ recent events with timestamps, icons, and color-coded status.
6
+
7
+ Each event is formatted with:
8
+ - Human-readable timestamp
9
+ - Event-type icon/label
10
+ - Short description from event data
11
+ - Color: green (success), red (failure), yellow (in-progress)
12
+
13
+ Auto-refreshes every 2 seconds via a Textual timer. Max 50 visible
14
+ items in a scrollable container.
15
+ """
16
+
17
+ if __name__ == "__main__":
18
+ print("This is a library file and is not meant to be run directly.")
19
+ import sys
20
+ sys.exit(1)
21
+
22
+ import datetime
23
+ import time
24
+
25
+ TEXTUAL_AVAILABLE = True
26
+ try:
27
+ from textual.containers import Vertical, ScrollView
28
+ from textual.widgets import Static
29
+ except Exception:
30
+ TEXTUAL_AVAILABLE = False
31
+
32
+ if TEXTUAL_AVAILABLE:
33
+ from . import theme_css
34
+ from .. import event_stream as _event_stream_mod
35
+
36
+
37
+ _EVENT_ICONS = {
38
+ _event_stream_mod.TOOL_STARTED: ("\U0001f527", "[TOOL]", "yellow"),
39
+ _event_stream_mod.TOOL_RESULT: ("\u2705", "[DONE]", "green"),
40
+ _event_stream_mod.FILE_CREATED: ("\u2795", "[CREATE]", "green"),
41
+ _event_stream_mod.FILE_EDITED: ("\u270f\ufe0f", "[EDIT]", "yellow"),
42
+ _event_stream_mod.FILE_DELETED: ("\u2716\ufe0f", "[DELETE]", "red"),
43
+ _event_stream_mod.FILE_MOVED: ("\u2b05\ufe0f", "[MOVE]", "yellow"),
44
+ _event_stream_mod.COMMAND_STARTED: ("\u25b6\ufe0f", "[CMD]", "yellow"),
45
+ _event_stream_mod.COMMAND_RESULT: ("\u2714\ufe0f", "[CMD_DONE]", "green"),
46
+ _event_stream_mod.AGENT_STARTED: ("\U0001f916", "[AGENT]", "yellow"),
47
+ _event_stream_mod.AGENT_COMPLETED: ("\U0001f3c6", "[AGENT_DONE]", "green"),
48
+ _event_stream_mod.AGENT_FAILED: ("\u274c", "[AGENT_FAIL]", "red"),
49
+ _event_stream_mod.MEMORY_CREATED: ("\U0001f4be", "[MEM]", "green"),
50
+ _event_stream_mod.MEMORY_UPDATED: ("\U0001f504", "[MEM_UPD]", "yellow"),
51
+ _event_stream_mod.MEMORY_DELETED: ("\U0001f5d1\ufe0f", "[MEM_DEL]", "red"),
52
+ _event_stream_mod.MEMORY_READ: ("\U0001f4d6", "[MEM_READ]", "text-muted"),
53
+ _event_stream_mod.MODEL_RESPONSE: ("\U0001f4ac", "[RESP]", "text-muted"),
54
+ _event_stream_mod.TOOL_REQUEST: ("\U0001f4e1", "[REQ]", "yellow"),
55
+ _event_stream_mod.SESSION_CLEARED: ("\U0001f5d1\ufe0f", "[CLEAR]", "red"),
56
+ _event_stream_mod.CONTEXT_SWITCHED: ("\U0001f504", "[CTX]", "yellow"),
57
+ }
58
+
59
+ _MAX_VISIBLE = 50
60
+
61
+
62
+ def _fmt_ts(ts):
63
+ """Format a UNIX timestamp into a short human-readable string."""
64
+ try:
65
+ dt = datetime.datetime.fromtimestamp(ts)
66
+ now = datetime.datetime.now()
67
+ if dt.date() == now.date():
68
+ return dt.strftime("%H:%M:%S")
69
+ return dt.strftime("%m-%d %H:%M")
70
+ except Exception:
71
+ return "??:??:??"
72
+
73
+
74
+ class ActivityStreamPanel(Static):
75
+ """Embedded activity stream widget — mount this inside a
76
+ layout to get a live scrolling feed of event_stream events."""
77
+
78
+ CSS = """
79
+ ActivityStreamPanel {
80
+ height: 1fr;
81
+ width: 1fr;
82
+ background: $surface;
83
+ border: round $border;
84
+ padding: 0;
85
+ overflow: hidden;
86
+ }
87
+ #as-header {
88
+ height: 1;
89
+ padding: 0 1;
90
+ border-bottom: solid $border;
91
+ background: $surface-alt;
92
+ }
93
+ #as-header Label {
94
+ text-style: bold;
95
+ }
96
+ #as-body {
97
+ height: 1fr;
98
+ overflow-y: auto;
99
+ padding: 0 1;
100
+ }
101
+ .as-event {
102
+ height: auto;
103
+ min-height: 1;
104
+ padding: 0 0;
105
+ width: 1fr;
106
+ }
107
+ .as-event:hover {
108
+ background: $surface-alt;
109
+ }
110
+ #as-empty {
111
+ height: 3;
112
+ padding: 1 0;
113
+ color: $text-faint;
114
+ text-align: center;
115
+ }
116
+ #as-footer {
117
+ height: 1;
118
+ padding: 0 1;
119
+ border-top: solid $border;
120
+ color: $text-faint;
121
+ }
122
+ """
123
+
124
+ def __init__(self, **kwargs):
125
+ super().__init__(**kwargs)
126
+ self._refresh_timer = None
127
+ self._last_count = 0
128
+
129
+ def compose(self):
130
+ with Vertical(id="as-stream-wrap"):
131
+ yield Static(" \U0001f4ca Activity Stream", id="as-header")
132
+ yield ScrollView(id="as-body")
133
+ yield Static("", id="as-footer")
134
+
135
+ def on_mount(self):
136
+ from .. import event_stream
137
+ self._event_stream = event_stream.stream
138
+ self._refresh_timer = self.set_interval(2.0, self.refresh_stream)
139
+ self.refresh_stream()
140
+
141
+ def on_unmount(self):
142
+ if self._refresh_timer is not None:
143
+ self._refresh_timer.stop()
144
+ self._refresh_timer = None
145
+
146
+ def refresh_stream(self):
147
+ """Poll recent events from event_stream and update the display."""
148
+ try:
149
+ from .. import event_stream
150
+ events = event_stream.stream.get_recent(limit=_MAX_VISIBLE)
151
+ except Exception:
152
+ return
153
+
154
+ if not events:
155
+ try:
156
+ body = self.query_one("#as-body", ScrollView)
157
+ if not body.children:
158
+ body.mount(
159
+ Static("No activity yet.", id="as-empty"))
160
+ except Exception:
161
+ pass
162
+ return
163
+
164
+ if len(events) == self._last_count:
165
+ return
166
+ self._last_count = len(events)
167
+
168
+ try:
169
+ body = self.query_one("#as-body", ScrollView)
170
+ except Exception:
171
+ return
172
+
173
+ for child in list(body.children):
174
+ child.remove()
175
+
176
+ for ev in reversed(events[-_MAX_VISIBLE:]):
177
+ body.mount(Static(self._format_event(ev), classes="as-event"))
178
+
179
+ try:
180
+ footer = self.query_one("#as-footer", Static)
181
+ footer.update(f" {len(events)} event{'s' if len(events) != 1 else ''}")
182
+ except Exception:
183
+ pass
184
+
185
+ try:
186
+ body.scroll_end(animate=False)
187
+ except Exception:
188
+ pass
189
+
190
+ def _format_event(self, ev):
191
+ """Format a single event as a Rich-markup string."""
192
+ icon_tuple = _EVENT_ICONS.get(
193
+ ev.event_type, ("\u2753", f"[{ev.event_type}]", "text-muted"))
194
+ emoji, label, color = icon_tuple
195
+
196
+ ts_str = _fmt_ts(ev.timestamp)
197
+
198
+ desc = ev.data.get("description", "")
199
+ if not desc:
200
+ path = ev.data.get("path") or ev.data.get("file", "")
201
+ tool = ev.data.get("tool", "")
202
+ cmd = ev.data.get("command", "")
203
+ agent = ev.data.get("agent", "")
204
+ if path:
205
+ desc = str(path)
206
+ elif tool:
207
+ desc = str(tool)
208
+ elif cmd:
209
+ desc = str(cmd)[:40]
210
+ elif agent:
211
+ desc = str(agent)
212
+ else:
213
+ desc = ev.event_type
214
+
215
+ try:
216
+ hex_color = theme_css.current_hex(color)
217
+ except Exception:
218
+ hex_color = theme_css.current_hex("text-muted")
219
+
220
+ return (
221
+ f"[{hex_color}]{ts_str}[/] "
222
+ f"[{hex_color}]{emoji}[/] "
223
+ f"[{hex_color}]{desc}[/]"
224
+ )
225
+
226
+ def clear(self):
227
+ """Clear the displayed event list."""
228
+ try:
229
+ body = self.query_one("#as-body", ScrollView)
230
+ for child in list(body.children):
231
+ child.remove()
232
+ body.mount(Static("Stream cleared.", id="as-empty"))
233
+ except Exception:
234
+ pass
235
+ self._last_count = 0
236
+
237
+ else:
238
+ ActivityStreamPanel = None
@@ -0,0 +1,122 @@
1
+ """
2
+ CCT UI — animations.py (spec v0.7 project structure list).
3
+
4
+ The v0.7 spec asks for a `ui/animations.py` file with Thinking/
5
+ Searching/Responding indicators and live elapsed time. That engine
6
+ already existed and already matched the spec exactly — see
7
+ thinking.py's docstring ("Advanced AI Response Animation System",
8
+ spec item #25) — just under a different filename, wired into
9
+ composer.py's live status row and conversation.py's completion
10
+ summary.
11
+
12
+ Per the v0.7 brief ("Do NOT rewrite working modules"), this file does
13
+ not reimplement that logic a second time. It re-exports thinking.py's
14
+ public surface under the name the new architecture list expects, and
15
+ adds one small piece that genuinely didn't exist yet: an `AnimatedTick`
16
+ helper Textual widgets can use to self-drive a spinner + elapsed-timer
17
+ label without each caller hand-rolling its own `set_interval` bookkeeping.
18
+ """
19
+
20
+ if __name__ == "__main__":
21
+ print("This is a library file and is not meant to be run directly.")
22
+ import sys
23
+ sys.exit(1)
24
+
25
+ import time
26
+
27
+ from .thinking import ( # noqa: F401 (re-exported, not unused)
28
+ DOT_FRAMES, BRAILLE_FRAMES, BLOCK_FRAMES, STYLES,
29
+ STAGES, DEFAULT_STAGE, IDLE_ICONS,
30
+ spinner_frame, detect_stage, stage_info, format_timer,
31
+ idle_icon, commands_used, completion_summary_lines,
32
+ )
33
+
34
+ TEXTUAL_AVAILABLE = True
35
+ try:
36
+ from textual.widgets import Static
37
+ except Exception:
38
+ TEXTUAL_AVAILABLE = False
39
+
40
+
41
+ if TEXTUAL_AVAILABLE:
42
+
43
+ class AnimatedTick(Static):
44
+ """A self-driving `<spinner frame> <Stage Label> <elapsed>s`
45
+ line — Thinking / Searching / Responding, matching the spec's
46
+ three named animation styles via thinking.stage_info()'s style
47
+ field. Callers set the stage with `set_stage(key, prompt=...)`;
48
+ this widget owns its own interval and elapsed-time math so nothing
49
+ else has to poll it.
50
+ """
51
+
52
+ def __init__(self, stage=DEFAULT_STAGE, id=None):
53
+ super().__init__("", id=id, classes="cct-animated-tick")
54
+ self._stage = stage
55
+ self._tick = 0
56
+ self._started = time.time()
57
+ self._chunk_count = 0
58
+ self._running = False
59
+ self._interval = None
60
+
61
+ def on_mount(self):
62
+ self._redraw()
63
+
64
+ def on_unmount(self):
65
+ self.stop()
66
+
67
+ def start(self, stage=None):
68
+ self._started = time.time()
69
+ self._chunk_count = 0
70
+ self._tick = 0
71
+ if stage:
72
+ self._stage = stage
73
+ self._running = True
74
+ self._redraw()
75
+ # Never stack a second 8Hz ticker on repeated start():
76
+ # stop the previous interval first (mirrors cat_agent's
77
+ # duplicate guard).
78
+ try:
79
+ if self._interval is not None:
80
+ self._interval.stop()
81
+ except Exception:
82
+ pass
83
+ self._interval = self.set_interval(1 / 8, self._on_tick)
84
+
85
+ def stop(self):
86
+ self._running = False
87
+ try:
88
+ if self._interval is not None:
89
+ self._interval.stop()
90
+ except Exception:
91
+ pass
92
+ self._interval = None
93
+
94
+ def note_chunk(self):
95
+ """Call once per streamed chunk so elapsed-time-based stage
96
+ progression (thinking.detect_stage) can tell 'still waiting
97
+ for the first token' from 'tokens are already arriving'."""
98
+ self._chunk_count += 1
99
+
100
+ def set_stage(self, stage_key):
101
+ self._stage = stage_key
102
+ self._redraw()
103
+
104
+ def elapsed(self):
105
+ return time.time() - self._started
106
+
107
+ def _on_tick(self):
108
+ self._tick += 1
109
+ self._redraw()
110
+
111
+ def _redraw(self):
112
+ label, color, style = stage_info(self._stage)
113
+ frame = spinner_frame(style, self._tick)
114
+ elapsed_txt = format_timer(self.elapsed()) if self._running else ""
115
+ text = f"[{color}]{frame} {label}"
116
+ if elapsed_txt:
117
+ text += f" {elapsed_txt}"
118
+ text += "[/]"
119
+ self.update(text)
120
+
121
+ else:
122
+ AnimatedTick = None