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,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
|