algo-cli-runtime 0.14.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 (237) hide show
  1. algo_cli/__init__.py +3 -0
  2. algo_cli/__main__.py +7 -0
  3. algo_cli/_internal/__init__.py +12 -0
  4. algo_cli/_internal/policy_chain.py +259 -0
  5. algo_cli/action_registry.py +1047 -0
  6. algo_cli/agent_blocks.py +550 -0
  7. algo_cli/agent_pipeline.py +1457 -0
  8. algo_cli/agent_threads.py +308 -0
  9. algo_cli/animations.py +316 -0
  10. algo_cli/cache_admission.py +209 -0
  11. algo_cli/capability_mask.py +66 -0
  12. algo_cli/chat_protocol.py +116 -0
  13. algo_cli/chatgpt_auth.py +510 -0
  14. algo_cli/chatgpt_client.py +657 -0
  15. algo_cli/code_rag.py +479 -0
  16. algo_cli/config.py +651 -0
  17. algo_cli/context_budget.py +679 -0
  18. algo_cli/credential_helpers.py +315 -0
  19. algo_cli/deliberation.py +29 -0
  20. algo_cli/display.py +1470 -0
  21. algo_cli/evals/__init__.py +21 -0
  22. algo_cli/evals/algorithm_effectiveness.py +560 -0
  23. algo_cli/evals/competitive_harness_rating.py +702 -0
  24. algo_cli/evals/cot_quality.py +220 -0
  25. algo_cli/evals/harness_retrieval_benchmark.py +401 -0
  26. algo_cli/evals/performance_regression.py +136 -0
  27. algo_cli/evals/scorecard_grading.py +308 -0
  28. algo_cli/evals/session_distribution.py +84 -0
  29. algo_cli/execution_guardrails.py +806 -0
  30. algo_cli/extensions_manifest.py +84 -0
  31. algo_cli/git_evidence.py +227 -0
  32. algo_cli/google_workspace.py +407 -0
  33. algo_cli/google_workspace_auth.py +523 -0
  34. algo_cli/harness.py +2587 -0
  35. algo_cli/identity.py +557 -0
  36. algo_cli/index_compute_lab.py +228 -0
  37. algo_cli/inference_harness.py +70 -0
  38. algo_cli/intelligence/__init__.py +1103 -0
  39. algo_cli/intelligence/acrobat_config.py +307 -0
  40. algo_cli/intelligence/acrobat_manifests.py +338 -0
  41. algo_cli/intelligence/acrobat_models.py +195 -0
  42. algo_cli/intelligence/acrobat_pipeline.py +295 -0
  43. algo_cli/intelligence/acrobat_runtime.py +302 -0
  44. algo_cli/intelligence/acrobat_security.py +261 -0
  45. algo_cli/intelligence/acrobat_workflows.py +226 -0
  46. algo_cli/intelligence/actionability.py +165 -0
  47. algo_cli/intelligence/adversarial_audit.py +136 -0
  48. algo_cli/intelligence/agent_arena.py +92 -0
  49. algo_cli/intelligence/agent_benchmark.py +236 -0
  50. algo_cli/intelligence/agent_runtime.py +171 -0
  51. algo_cli/intelligence/agents_as_tools.py +70 -0
  52. algo_cli/intelligence/artifact_binding.py +80 -0
  53. algo_cli/intelligence/autonomous_engineer.py +1976 -0
  54. algo_cli/intelligence/backpressure.py +99 -0
  55. algo_cli/intelligence/bloom_filter.py +186 -0
  56. algo_cli/intelligence/bonferroni.py +66 -0
  57. algo_cli/intelligence/boundary_compaction.py +98 -0
  58. algo_cli/intelligence/catalog_verifier.py +172 -0
  59. algo_cli/intelligence/cavecrew.py +118 -0
  60. algo_cli/intelligence/changelog.py +176 -0
  61. algo_cli/intelligence/checkpoint_resume.py +92 -0
  62. algo_cli/intelligence/circuit_breaker.py +88 -0
  63. algo_cli/intelligence/clarification_gate.py +101 -0
  64. algo_cli/intelligence/code_graph.py +180 -0
  65. algo_cli/intelligence/coderank.py +97 -0
  66. algo_cli/intelligence/consistent_hash.py +150 -0
  67. algo_cli/intelligence/consortium_synthesis.py +139 -0
  68. algo_cli/intelligence/construction/__init__.py +241 -0
  69. algo_cli/intelligence/construction/common.py +273 -0
  70. algo_cli/intelligence/construction/documents.py +496 -0
  71. algo_cli/intelligence/construction/labor_units.py +1395 -0
  72. algo_cli/intelligence/construction/payments.py +470 -0
  73. algo_cli/intelligence/construction/risk.py +784 -0
  74. algo_cli/intelligence/content_extractor.py +132 -0
  75. algo_cli/intelligence/context_adaptive.py +102 -0
  76. algo_cli/intelligence/context_ops.py +95 -0
  77. algo_cli/intelligence/count_min.py +145 -0
  78. algo_cli/intelligence/cow_state.py +103 -0
  79. algo_cli/intelligence/critic_loop.py +119 -0
  80. algo_cli/intelligence/cross_source.py +113 -0
  81. algo_cli/intelligence/daemon_mode.py +99 -0
  82. algo_cli/intelligence/dag_orchestration.py +151 -0
  83. algo_cli/intelligence/deep_research.py +155 -0
  84. algo_cli/intelligence/degenerate_detector.py +78 -0
  85. algo_cli/intelligence/delta_report.py +92 -0
  86. algo_cli/intelligence/discovery_event_log.py +92 -0
  87. algo_cli/intelligence/document_ingest.py +298 -0
  88. algo_cli/intelligence/dual_layer_validate.py +151 -0
  89. algo_cli/intelligence/echo_fidelity.py +73 -0
  90. algo_cli/intelligence/ema_tuning.py +104 -0
  91. algo_cli/intelligence/event_log.py +92 -0
  92. algo_cli/intelligence/evidence_graph.py +114 -0
  93. algo_cli/intelligence/extension_host.py +162 -0
  94. algo_cli/intelligence/extension_manifest.py +115 -0
  95. algo_cli/intelligence/falsification_suite.py +178 -0
  96. algo_cli/intelligence/finance/__init__.py +169 -0
  97. algo_cli/intelligence/finance/anomalies.py +135 -0
  98. algo_cli/intelligence/finance/ap_ar.py +351 -0
  99. algo_cli/intelligence/finance/cash.py +162 -0
  100. algo_cli/intelligence/finance/close.py +332 -0
  101. algo_cli/intelligence/finance/common.py +244 -0
  102. algo_cli/intelligence/finance/construction.py +135 -0
  103. algo_cli/intelligence/finance/controls.py +172 -0
  104. algo_cli/intelligence/finance/evidence.py +119 -0
  105. algo_cli/intelligence/finance/exceptions.py +157 -0
  106. algo_cli/intelligence/finance/reconciliations.py +254 -0
  107. algo_cli/intelligence/finance/revenue.py +109 -0
  108. algo_cli/intelligence/finance/tax.py +74 -0
  109. algo_cli/intelligence/finance/workpapers.py +111 -0
  110. algo_cli/intelligence/finding_record.py +120 -0
  111. algo_cli/intelligence/flow_dag.py +267 -0
  112. algo_cli/intelligence/gatherer.py +223 -0
  113. algo_cli/intelligence/golden_master.py +98 -0
  114. algo_cli/intelligence/graph_rag.py +195 -0
  115. algo_cli/intelligence/group_chat.py +143 -0
  116. algo_cli/intelligence/hash_dedup.py +145 -0
  117. algo_cli/intelligence/hyperloglog.py +128 -0
  118. algo_cli/intelligence/incremental_index.py +316 -0
  119. algo_cli/intelligence/index_store.py +16 -0
  120. algo_cli/intelligence/iteration_plan.py +133 -0
  121. algo_cli/intelligence/kernel_plugins.py +167 -0
  122. algo_cli/intelligence/lesson_catalog.py +135 -0
  123. algo_cli/intelligence/llm_fallback.py +169 -0
  124. algo_cli/intelligence/log2_histogram.py +267 -0
  125. algo_cli/intelligence/lsp_integration.py +147 -0
  126. algo_cli/intelligence/memory_evolution.py +117 -0
  127. algo_cli/intelligence/minhash_lsh.py +182 -0
  128. algo_cli/intelligence/multi_model_score.py +174 -0
  129. algo_cli/intelligence/multi_tier_grade.py +211 -0
  130. algo_cli/intelligence/negative_controls.py +113 -0
  131. algo_cli/intelligence/numeric_clamp.py +63 -0
  132. algo_cli/intelligence/occ_editor.py +66 -0
  133. algo_cli/intelligence/output_normalize.py +112 -0
  134. algo_cli/intelligence/parallel_delegation.py +98 -0
  135. algo_cli/intelligence/parallel_fanout.py +104 -0
  136. algo_cli/intelligence/permission_modes.py +105 -0
  137. algo_cli/intelligence/pre_push_gate.py +68 -0
  138. algo_cli/intelligence/prefetch.py +171 -0
  139. algo_cli/intelligence/process_framework.py +217 -0
  140. algo_cli/intelligence/project_graph.py +387 -0
  141. algo_cli/intelligence/query_expansion.py +146 -0
  142. algo_cli/intelligence/ralph_loop.py +117 -0
  143. algo_cli/intelligence/rate_limiter.py +153 -0
  144. algo_cli/intelligence/refactor_transaction.py +94 -0
  145. algo_cli/intelligence/research_workspace.py +108 -0
  146. algo_cli/intelligence/retraction_ledger.py +72 -0
  147. algo_cli/intelligence/saga_pattern.py +88 -0
  148. algo_cli/intelligence/session_fork.py +100 -0
  149. algo_cli/intelligence/shadow_editor.py +67 -0
  150. algo_cli/intelligence/shell_session.py +213 -0
  151. algo_cli/intelligence/source_registry.py +143 -0
  152. algo_cli/intelligence/spawn_scales.py +99 -0
  153. algo_cli/intelligence/stat_stability.py +104 -0
  154. algo_cli/intelligence/structural_validator.py +148 -0
  155. algo_cli/intelligence/subagent_spawner.py +111 -0
  156. algo_cli/intelligence/symmetric_verify.py +70 -0
  157. algo_cli/intelligence/task_classifier.py +129 -0
  158. algo_cli/intelligence/team_execution.py +122 -0
  159. algo_cli/intelligence/tiered_access.py +121 -0
  160. algo_cli/intelligence/utility_registry.py +159 -0
  161. algo_cli/intuition_engine.py +560 -0
  162. algo_cli/intuition_injector.py +82 -0
  163. algo_cli/kernels/__init__.py +5 -0
  164. algo_cli/kernels/manifest.py +763 -0
  165. algo_cli/main.py +3903 -0
  166. algo_cli/memory_candidates.py +541 -0
  167. algo_cli/memory_echo_veil.py +394 -0
  168. algo_cli/memory_runtime.py +112 -0
  169. algo_cli/model_info.py +548 -0
  170. algo_cli/model_profile.py +160 -0
  171. algo_cli/model_routing.py +74 -0
  172. algo_cli/oneshot.py +331 -0
  173. algo_cli/perf_telemetry.py +389 -0
  174. algo_cli/plugins.py +245 -0
  175. algo_cli/private_event_store.py +654 -0
  176. algo_cli/quantization/__init__.py +24 -0
  177. algo_cli/quantization/lloyd_max.py +98 -0
  178. algo_cli/quantization/turbo_quant.py +308 -0
  179. algo_cli/reasoning/__init__.py +46 -0
  180. algo_cli/reasoning/combinatorial.py +356 -0
  181. algo_cli/reasoning/graph_of_thought.py +297 -0
  182. algo_cli/reasoning/mcts.py +220 -0
  183. algo_cli/reasoning/neuro_symbolic.py +250 -0
  184. algo_cli/reasoning/react.py +246 -0
  185. algo_cli/reasoning/reflexion.py +225 -0
  186. algo_cli/reasoning/tree_of_thought.py +241 -0
  187. algo_cli/reasoning_bridge.py +150 -0
  188. algo_cli/reconciliation.py +284 -0
  189. algo_cli/reflex.py +385 -0
  190. algo_cli/resources/docs/ALGO.md +13958 -0
  191. algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
  192. algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
  193. algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
  194. algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
  195. algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
  196. algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
  197. algo_cli/resources/docs/main-split-map.md +35 -0
  198. algo_cli/resources/docs/privacy-and-context.md +48 -0
  199. algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
  200. algo_cli/resources/skills/README.md +26 -0
  201. algo_cli/resources/skills/algo-cli.md +59 -0
  202. algo_cli/resources/skills/edit-file-precision.md +49 -0
  203. algo_cli/resources/skills/harness-search-first.md +47 -0
  204. algo_cli/resources/skills/memory-recall-ritual.md +51 -0
  205. algo_cli/resources/skills/qol-algorithms.md +224 -0
  206. algo_cli/resources/skills/smart-error-recovery.md +56 -0
  207. algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
  208. algo_cli/retrieval_algorithms.py +127 -0
  209. algo_cli/runtime_qos.py +236 -0
  210. algo_cli/runtime_services.py +320 -0
  211. algo_cli/session_commands.py +95 -0
  212. algo_cli/session_mode.py +113 -0
  213. algo_cli/skills.py +430 -0
  214. algo_cli/slash_dispatch.py +1265 -0
  215. algo_cli/small_context.py +206 -0
  216. algo_cli/spawn_budget.py +89 -0
  217. algo_cli/task_ledger.py +84 -0
  218. algo_cli/task_router.py +197 -0
  219. algo_cli/tool_context.py +94 -0
  220. algo_cli/tool_contract.py +99 -0
  221. algo_cli/tool_policy.py +357 -0
  222. algo_cli/tool_runtime.py +647 -0
  223. algo_cli/tools.py +3056 -0
  224. algo_cli/url_scheme.py +174 -0
  225. algo_cli/verify.py +154 -0
  226. algo_cli/version_manifest.py +178 -0
  227. algo_cli/vision_screenshot_verify.py +76 -0
  228. algo_cli/workspace_resolver.py +68 -0
  229. algo_cli/x_account.py +209 -0
  230. algo_cli/xai_auth.py +374 -0
  231. algo_cli/xai_client.py +600 -0
  232. algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
  233. algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
  234. algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
  235. algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
  236. algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
  237. ollama_cli/__init__.py +67 -0
algo_cli/display.py ADDED
@@ -0,0 +1,1470 @@
1
+ """Rich terminal display for the CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import time
8
+ from contextlib import contextmanager
9
+ from contextvars import ContextVar
10
+ from pathlib import Path
11
+ from typing import Any, Iterator
12
+
13
+ from rich.align import Align
14
+ from rich.console import Capture, Console
15
+ from rich.console import Group
16
+ from rich.live import Live
17
+ from rich.markdown import Markdown
18
+ from rich.panel import Panel
19
+ from rich import box
20
+ from rich.table import Table
21
+ from rich.text import Text
22
+ from rich.theme import Theme
23
+
24
+ from algo_cli.animations import (
25
+ AIState,
26
+ animation_for,
27
+ buddy_frame,
28
+ current_frame,
29
+ glyph,
30
+ register_spinners,
31
+ spinner_name,
32
+ state_line,
33
+ )
34
+
35
+ try:
36
+ from algo_cli import __version__ as _CLI_VERSION
37
+ except Exception:
38
+ _CLI_VERSION = "dev"
39
+
40
+ try:
41
+ from algo_cli.config import CONFIG_DIR, _atomic_write_text
42
+ except Exception:
43
+ CONFIG_DIR = Path.home() / ".algo_cli"
44
+ _atomic_write_text = None # type: ignore[assignment]
45
+
46
+
47
+ DEFAULT_THEME_NAME = (
48
+ os.environ.get("ALGO_CLI_THEME")
49
+ or os.environ.get("OLLAMA_CLI_THEME")
50
+ or "tokyo-night"
51
+ ).strip().lower() or "tokyo-night"
52
+
53
+
54
+ def _theme(styles: dict[str, str]) -> Theme:
55
+ return Theme(styles)
56
+
57
+
58
+ THEME_COLORS: dict[str, dict[str, str]] = {
59
+ "tokyo-night": {
60
+ "primary": "#7aa2f7",
61
+ "secondary": "#bb9af7",
62
+ "accent": "#2ac3de",
63
+ "surface": "#111827",
64
+ "surface_alt": "#0f172a",
65
+ "border": "#1f2a44",
66
+ "border_accent": "#3b4b72",
67
+ "text": "#e5e9f0",
68
+ "muted": "#7b88a8",
69
+ "success": "#9ece6a",
70
+ "warning": "#e0af68",
71
+ "error": "#f7768e",
72
+ "info": "#7dcfff",
73
+ },
74
+ "catppuccin-mocha": {
75
+ "primary": "#89b4fa",
76
+ "secondary": "#cba6f7",
77
+ "accent": "#94e2d5",
78
+ "surface": "#11111b",
79
+ "surface_alt": "#1e1e2e",
80
+ "border": "#313244",
81
+ "border_accent": "#45475a",
82
+ "text": "#cdd6f4",
83
+ "muted": "#6c7086",
84
+ "success": "#a6e3a1",
85
+ "warning": "#f9e2af",
86
+ "error": "#f38ba8",
87
+ "info": "#89dceb",
88
+ },
89
+ "dracula": {
90
+ "primary": "#bd93f9",
91
+ "secondary": "#ff79c6",
92
+ "accent": "#8be9fd",
93
+ "surface": "#282a36",
94
+ "surface_alt": "#1e1f29",
95
+ "border": "#44475a",
96
+ "border_accent": "#6272a4",
97
+ "text": "#f8f8f2",
98
+ "muted": "#6272a4",
99
+ "success": "#50fa7b",
100
+ "warning": "#f1fa8c",
101
+ "error": "#ff5555",
102
+ "info": "#8be9fd",
103
+ },
104
+ "nord": {
105
+ "primary": "#88c0d0",
106
+ "secondary": "#b48ead",
107
+ "accent": "#81a1c1",
108
+ "surface": "#2e3440",
109
+ "surface_alt": "#3b4252",
110
+ "border": "#4c566a",
111
+ "border_accent": "#5e81ac",
112
+ "text": "#eceff4",
113
+ "muted": "#81a1c1",
114
+ "success": "#a3be8c",
115
+ "warning": "#ebcb8b",
116
+ "error": "#bf616a",
117
+ "info": "#8fbcbb",
118
+ },
119
+ "gruvbox": {
120
+ "primary": "#83a598",
121
+ "secondary": "#d3869b",
122
+ "accent": "#8ec07c",
123
+ "surface": "#282828",
124
+ "surface_alt": "#3c3836",
125
+ "border": "#504945",
126
+ "border_accent": "#665c54",
127
+ "text": "#ebdbb2",
128
+ "muted": "#928374",
129
+ "success": "#b8bb26",
130
+ "warning": "#fabd2f",
131
+ "error": "#fb4934",
132
+ "info": "#83a598",
133
+ },
134
+ # Brand theme matching the Redeye mark: bold red on near-black,
135
+ # high contrast, minimal mid-tones.
136
+ "redeye": {
137
+ "primary": "#e22b2b",
138
+ "secondary": "#ff6b6b",
139
+ "accent": "#ff3b3b",
140
+ "surface": "#0a0a0a",
141
+ "surface_alt": "#111111",
142
+ "border": "#2a1212",
143
+ "border_accent": "#7a1f1f",
144
+ "text": "#f2f2f2",
145
+ "muted": "#8a7070",
146
+ "success": "#6fcf6f",
147
+ "warning": "#ffb347",
148
+ "error": "#ff2e2e",
149
+ "info": "#ff8a8a",
150
+ },
151
+ "dolphie": {
152
+ "primary": "#bbc8e8",
153
+ "secondary": "#91abec",
154
+ "accent": "#8f9fc1",
155
+ "surface": "#0f1525",
156
+ "surface_alt": "#0a0e1b",
157
+ "border": "#1b233a",
158
+ "border_accent": "#32416a",
159
+ "text": "#e9e9e9",
160
+ "muted": "#5e6b87",
161
+ "success": "#54efae",
162
+ "warning": "#f0e357",
163
+ "error": "#f05757",
164
+ "info": "#8f9fc1",
165
+ },
166
+ }
167
+
168
+ THEME_MAP: dict[str, Theme] = {name: _theme(colors) for name, colors in THEME_COLORS.items()}
169
+
170
+ _base_theme_name = DEFAULT_THEME_NAME if DEFAULT_THEME_NAME in THEME_MAP else "tokyo-night"
171
+ _active_theme_name = _base_theme_name
172
+ _theme_pushed = False
173
+ console = Console(theme=THEME_MAP[_base_theme_name])
174
+ register_spinners() # make algo-* state spinners available to console.status
175
+ _stream_live: Live | None = None
176
+ _stream_buffer = ""
177
+ _stream_pending = ""
178
+ _last_render_time = 0.0
179
+ _RENDER_INTERVAL = 1.0 / 12 # cap Markdown re-parses at 12 fps regardless of token rate
180
+ _thinking_live: Live | None = None
181
+ _thinking_buffer = ""
182
+ _thinking_pending = ""
183
+ _thinking_last_render_time = 0.0
184
+ _thinking_started_at = 0.0
185
+ _THINKING_VISIBLE_CHARS = 1200
186
+
187
+ # One-shot JSON event sink. When set, display helpers route to it instead of
188
+ # rendering Rich panels. Installed by algo_cli.oneshot.run_oneshot().
189
+ _json_sink: Any = None
190
+ _console_capture_active: ContextVar[bool] = ContextVar(
191
+ "algo_cli_console_capture_active",
192
+ default=False,
193
+ )
194
+
195
+
196
+ def install_json_sink(sink: Any) -> None:
197
+ global _json_sink
198
+ _json_sink = sink
199
+
200
+
201
+ def uninstall_json_sink() -> None:
202
+ global _json_sink
203
+ _json_sink = None
204
+
205
+
206
+ def json_sink() -> Any:
207
+ return _json_sink
208
+
209
+
210
+ def json_mode_active() -> bool:
211
+ """True when --oneshot --json (or another bridge) owns stdout."""
212
+ return _json_sink is not None
213
+
214
+
215
+ @contextmanager
216
+ def capture_console_output() -> Iterator[Capture]:
217
+ """Capture this context's Rich output, including JSON-suppressed notices.
218
+
219
+ Rich stores capture buffers per thread, and the ContextVar keeps async/task
220
+ contexts isolated. Direct interactive rendering is therefore unaffected.
221
+ """
222
+
223
+ token = _console_capture_active.set(True)
224
+ try:
225
+ with console.capture() as captured:
226
+ yield captured
227
+ finally:
228
+ _console_capture_active.reset(token)
229
+
230
+
231
+ @contextmanager
232
+ def tool_execution_status(label: str, *, spinner: str | None = None) -> Iterator[None]:
233
+ """Rich spinner for interactive mode only; no-op for JSON/bridge consumers.
234
+
235
+ Defaults to the branded DOING progress sweep; pass any rich spinner name
236
+ to override.
237
+ """
238
+ if _json_sink is not None:
239
+ yield
240
+ return
241
+ doing = animation_for(AIState.DOING)
242
+ with console.status(label, spinner=spinner or spinner_name(AIState.DOING), spinner_style=doing.style):
243
+ yield
244
+
245
+
246
+ def available_themes() -> list[str]:
247
+ return sorted(THEME_MAP)
248
+
249
+
250
+ def current_theme_name() -> str:
251
+ return _active_theme_name
252
+
253
+
254
+ def set_theme(name: str) -> str:
255
+ global _active_theme_name, _theme_pushed
256
+
257
+ candidate = (name or "").strip().lower().replace("_", "-")
258
+ if not candidate:
259
+ candidate = "tokyo-night"
260
+ if candidate not in THEME_MAP:
261
+ raise ValueError(f"Unknown theme: {name}")
262
+ if candidate == _active_theme_name:
263
+ return candidate
264
+ if _theme_pushed:
265
+ console.pop_theme()
266
+ _theme_pushed = False
267
+ if candidate != _base_theme_name:
268
+ console.push_theme(THEME_MAP[candidate])
269
+ _theme_pushed = True
270
+ _active_theme_name = candidate
271
+ return candidate
272
+
273
+
274
+ def theme_colors(name: str | None = None) -> dict[str, str]:
275
+ candidate = (name or _active_theme_name or _base_theme_name).strip().lower().replace("_", "-")
276
+ if candidate not in THEME_COLORS:
277
+ candidate = _base_theme_name
278
+ return THEME_COLORS[candidate]
279
+
280
+
281
+ # Block logo (ansi_shadow-style), matching Hermes Agent banner weight.
282
+ _ALGO_CLI_LOGO: tuple[str, ...] = (
283
+ " █████╗ ██╗ ██████╗ ██████╗ ██████╗██╗ ██╗",
284
+ "██╔══██╗██║ ██╔════╝ ██╔═══██╗ ██╔════╝██║ ██║",
285
+ "███████║██║ ██║ ███╗██║ ██║ ██║ ██║ ██║",
286
+ "██╔══██║██║ ██║ ██║██║ ██║ ██║ ██║ ██║",
287
+ "██║ ██║███████╗╚██████╔╝╚██████╔╝ ╚██████╗███████╗██║",
288
+ "╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝╚══════╝╚═╝",
289
+ )
290
+
291
+ _PRODUCT_TAGLINE = "Agent runtime for tools, durable context, and verified work."
292
+ _BANNER_CAPABILITIES: tuple[tuple[str, str], ...] = (
293
+ ("Inference", "Ollama local/cloud · xAI Grok · ChatGPT/Codex"),
294
+ ("Context", "skills · memory · repository intelligence · knowledge graph"),
295
+ ("Workflows", "direct chat · routed agent pipelines · one-shot JSON"),
296
+ )
297
+
298
+
299
+ def _banner_flow_line() -> Text:
300
+ line = Text()
301
+ for index, (label, style) in enumerate((
302
+ ("understand", "secondary"),
303
+ ("route", "primary"),
304
+ ("act", "accent"),
305
+ ("verify", "success"),
306
+ ("remember", "secondary"),
307
+ )):
308
+ if index:
309
+ line.append(" → ", style="muted")
310
+ line.append(label, style=f"bold {style}")
311
+ return line
312
+
313
+
314
+ def _banner_body_layout() -> Group:
315
+ capabilities = Table.grid(padding=(0, 2), expand=True)
316
+ capabilities.add_column(width=10, style="muted", no_wrap=True)
317
+ capabilities.add_column(ratio=1, style="text")
318
+ for label, detail in _BANNER_CAPABILITIES:
319
+ capabilities.add_row(Text(label, style="bold secondary"), detail)
320
+ return Group(
321
+ Text(_PRODUCT_TAGLINE, style="text"),
322
+ Text(""),
323
+ _banner_flow_line(),
324
+ Text(""),
325
+ capabilities,
326
+ )
327
+
328
+
329
+ def _print_algo_logo() -> None:
330
+ if console.width < 70:
331
+ console.print(Align.center(Text.assemble(
332
+ ("ALGO", "bold primary"),
333
+ (" / ", "muted"),
334
+ ("CLI", "bold secondary"),
335
+ )))
336
+ return
337
+ for line in _ALGO_CLI_LOGO:
338
+ console.print(Align.center(Text(line.rstrip(), style="bold primary")))
339
+
340
+
341
+ def render_opening_banner(*, version: str | None = None) -> Panel:
342
+ version_line = version or _CLI_VERSION
343
+ panel_title = (
344
+ f"[bold primary]Algo CLI[/] [muted]v{version_line}[/] "
345
+ f"[muted]·[/] [accent]agent runtime[/]"
346
+ )
347
+ return Panel(
348
+ _banner_body_layout(),
349
+ title=panel_title,
350
+ subtitle="[muted]ask naturally · type / for commands[/]",
351
+ border_style="border_accent",
352
+ box=box.ROUNDED,
353
+ padding=(1, 2),
354
+ )
355
+
356
+
357
+ def show_banner() -> None:
358
+ if json_mode_active():
359
+ return
360
+ console.print()
361
+ _print_algo_logo()
362
+ console.print()
363
+
364
+ console.print(render_opening_banner())
365
+ console.print(state_line(AIState.IDLE, "ready — ask naturally or type /"))
366
+ console.print()
367
+
368
+
369
+ def compact_path(path: str, limit: int = 42) -> str:
370
+ text = str(path).strip()
371
+ if len(text) <= limit:
372
+ return text
373
+ p = Path(text)
374
+ parts = p.parts
375
+ if len(parts) >= 2:
376
+ tail = Path(*parts[-2:]).as_posix().replace("/", "\\") if "\\" in text else Path(*parts[-2:]).as_posix()
377
+ if len(tail) + 1 < limit:
378
+ return f"...{tail}"
379
+ return f"{text[: max(1, limit - 3)]}..."
380
+
381
+
382
+ def _kv_table(rows: list[tuple[str, str]]) -> Table:
383
+ table = Table.grid(padding=(0, 1), expand=True)
384
+ table.add_column(style="muted", no_wrap=True)
385
+ table.add_column(style="text", overflow="ellipsis")
386
+ for label, value in rows:
387
+ table.add_row(f"{label}:", value)
388
+ return table
389
+
390
+
391
+ def _format_bytes(value: Any) -> str:
392
+ try:
393
+ size = int(value)
394
+ except Exception:
395
+ return str(value) if value is not None else "?"
396
+ units = ["B", "KB", "MB", "GB", "TB"]
397
+ amount = float(size)
398
+ for unit in units:
399
+ if amount < 1024 or unit == units[-1]:
400
+ return f"{amount:.1f} {unit}" if unit != "B" else f"{int(amount)} B"
401
+ amount /= 1024
402
+ return f"{size} B"
403
+
404
+
405
+ def _format_dt(value: Any) -> str:
406
+ if not value:
407
+ return "?"
408
+ try:
409
+ return value.strftime("%Y-%m-%d")
410
+ except Exception:
411
+ return str(value)
412
+
413
+
414
+ def _message_preview(message: dict[str, Any], limit: int = 220) -> str:
415
+ content = str(message.get("content") or message.get("thinking") or "").strip()
416
+ if not content:
417
+ return "(empty)"
418
+ content = " ".join(content.split())
419
+ return content if len(content) <= limit else content[: limit - 1] + "..."
420
+
421
+
422
+ def _render_model_rows(items: list[dict[str, str]], *, max_rows: int = 4) -> Table:
423
+ table = Table.grid(padding=(0, 1), expand=True)
424
+ table.add_column(style="text", ratio=2, overflow="ellipsis")
425
+ table.add_column(style="text", justify="right", no_wrap=True)
426
+ table.add_column(style="text", justify="right", no_wrap=True)
427
+ for row in items[:max_rows]:
428
+ table.add_row(
429
+ row.get("name", "?"),
430
+ row.get("size", "?"),
431
+ row.get("quant", "?"),
432
+ )
433
+ if not items:
434
+ table.add_row("[muted]No models found[/]", "", "")
435
+ return table
436
+
437
+
438
+ def _render_running_rows(items: list[dict[str, str]], *, max_rows: int = 3) -> Table:
439
+ table = Table.grid(padding=(0, 1), expand=True)
440
+ table.add_column(style="text", ratio=2, overflow="ellipsis")
441
+ table.add_column(style="text", justify="right", no_wrap=True)
442
+ table.add_column(style="text", justify="right", no_wrap=True)
443
+ for row in items[:max_rows]:
444
+ table.add_row(
445
+ row.get("name", "?"),
446
+ row.get("size_vram", row.get("size", "?")),
447
+ row.get("context", "?"),
448
+ )
449
+ if not items:
450
+ table.add_row("[muted]No running models[/]", "", "")
451
+ return table
452
+
453
+
454
+ def _render_chat_panel(cfg: Any) -> Panel:
455
+ recent = cfg.messages[-3:]
456
+ blocks: list[Any] = []
457
+ if not recent:
458
+ blocks.append(
459
+ Panel(
460
+ "[muted]No messages yet.[/]\n[dim]Type a prompt to start the session.[/]",
461
+ border_style="border",
462
+ box=box.ROUNDED,
463
+ )
464
+ )
465
+ else:
466
+ for message in recent:
467
+ role = str(message.get("role", "message")).upper()
468
+ title = role
469
+ if message.get("name"):
470
+ title = f"{role} {message['name']}"
471
+ border = "border_accent" if role == "ASSISTANT" else "border"
472
+ blocks.append(
473
+ Panel(
474
+ _message_preview(message),
475
+ title=title,
476
+ border_style=border,
477
+ box=box.ROUNDED,
478
+ )
479
+ )
480
+
481
+ num_ctx = getattr(cfg, "num_ctx", None)
482
+ ctx_label = str(num_ctx) if num_ctx is not None else "?"
483
+ hint_row = Text(
484
+ f"Tab panes Enter send Esc stop / filter temp {cfg.temperature} ctx {ctx_label}",
485
+ style="muted",
486
+ )
487
+ blocks.append(hint_row)
488
+ blocks.append(
489
+ Panel(
490
+ Text("draft: ready for a prompt or slash command", style="muted"),
491
+ border_style="border",
492
+ box=box.ROUNDED,
493
+ )
494
+ )
495
+ return Panel(Group(*blocks), title="Conversation", border_style="border_accent", box=box.ROUNDED)
496
+
497
+
498
+ def _render_inspector_panel(
499
+ cfg: Any,
500
+ *,
501
+ installed_models: list[dict[str, str]],
502
+ running_models: list[dict[str, str]],
503
+ event_lines: list[str],
504
+ ) -> Panel:
505
+ sections: list[Any] = []
506
+
507
+ session_box = _kv_table(
508
+ [
509
+ ("Current model", f"[bold primary]{cfg.model}[/]"),
510
+ ("Mode", f"[text]{'cloud' if cfg.cloud else 'local'}[/]"),
511
+ ("System prompt", f"[text]{(cfg.system.splitlines()[0] if cfg.system else '').strip() or 'default'}[/]"),
512
+ ("Parameters", f"[text]temp {cfg.temperature}, ctx {cfg.num_ctx}, reflect {max(1, int(cfg.tool_think_every))}[/]"),
513
+ ("cwd", f"[muted]{compact_path(cfg.cwd, 34)}[/]"),
514
+ ]
515
+ )
516
+ sections.append(Panel(session_box, title="Session", border_style="border", box=box.ROUNDED))
517
+
518
+ model_body = _render_model_rows(installed_models)
519
+ sections.append(Panel(model_body, title=f"Model {len(installed_models)} installed", border_style="border_accent", box=box.ROUNDED))
520
+
521
+ runtime_body = Table.grid(padding=(0, 1), expand=True)
522
+ runtime_body.add_column(style="text", ratio=2, overflow="ellipsis")
523
+ runtime_body.add_column(style="text", justify="right", no_wrap=True)
524
+ runtime_body.add_column(style="text", justify="right", no_wrap=True)
525
+ runtime_body.add_row("[bold]Server[/]", f"[info]{cfg.host}[/]", f"[text]{'cloud' if cfg.cloud else 'local'}[/]")
526
+ runtime_body.add_row("[bold]Context[/]", f"[text]{cfg.num_ctx}[/]", f"[text]{'summary' if cfg.session_summary.strip() else 'live'}[/]")
527
+ runtime_body.add_row("[bold]Running[/]", f"[text]{len(running_models)}[/]", f"[text]{'ready' if running_models else 'idle'}[/]")
528
+ if running_models:
529
+ runtime_body.add_row("", "", "")
530
+ runtime_body.add_row("[muted]Active[/]", f"[text]{running_models[0].get('name', '?')}[/]", f"[text]{running_models[0].get('size_vram', running_models[0].get('size', '?'))}[/]")
531
+ sections.append(Panel(runtime_body, title=f"Runtime {len(running_models)} running", border_style="border", box=box.ROUNDED))
532
+
533
+ actions_body = Text(
534
+ "/status Runtime summary\n"
535
+ "/doctor Readiness checks\n"
536
+ "/harness status Context health\n"
537
+ "/changes Last agent run\n"
538
+ "/help Command reference",
539
+ style="text",
540
+ )
541
+ sections.append(Panel(actions_body, title="Inspect", border_style="border", box=box.ROUNDED))
542
+
543
+ logs_body = Table.grid(padding=(0, 1), expand=True)
544
+ logs_body.add_column(style="muted", no_wrap=True)
545
+ logs_body.add_column(style="text", overflow="ellipsis")
546
+ if event_lines:
547
+ for index, line in enumerate(event_lines[:5], 1):
548
+ logs_body.add_row(f"{index:02d}", line)
549
+ else:
550
+ logs_body.add_row("01", "No events yet")
551
+ sections.append(Panel(logs_body, title="Logs", border_style="border", box=box.ROUNDED))
552
+
553
+ return Panel(Group(*sections), title="Runtime", border_style="border", box=box.ROUNDED)
554
+
555
+
556
+ def show_session_overview(
557
+ model: str,
558
+ host: str,
559
+ cwd: str,
560
+ theme_name: str,
561
+ cloud: bool,
562
+ auto_mode: bool,
563
+ safe_mode: bool,
564
+ temperature: float,
565
+ used_tokens: int,
566
+ total_tokens: int,
567
+ summary_active: bool,
568
+ tool_think_every: int,
569
+ max_tool_iterations: int,
570
+ memory_count: int,
571
+ system_prompt: str | None = None,
572
+ messages: list[dict[str, Any]] | None = None,
573
+ installed_models: list[dict[str, str]] | None = None,
574
+ running_models: list[dict[str, str]] | None = None,
575
+ event_lines: list[str] | None = None,
576
+ ) -> None:
577
+ used = min(max(used_tokens, 0), total_tokens) if total_tokens > 0 else used_tokens
578
+ remaining = max(total_tokens - used, 0) if total_tokens > 0 else 0
579
+ context_line = f"{used}/{total_tokens} ({int(round((remaining / total_tokens) * 100))}% left)" if total_tokens > 0 else "unknown"
580
+ mode_label = "cloud" if cloud else "local"
581
+ execution_label = "Ollama Cloud API" if cloud else host
582
+ installed_models = installed_models or []
583
+ running_models = running_models or []
584
+ event_lines = event_lines or []
585
+
586
+ if console.width < 100:
587
+ header_status: Any = _kv_table(
588
+ [
589
+ ("Connected", f"[text]{execution_label}[/]"),
590
+ ("Model", f"[bold primary]{model}[/]"),
591
+ ("Mode", f"[info]{mode_label}[/]"),
592
+ ("Context", f"[text]{context_line}[/]"),
593
+ ("Safety", f"[text]{'safe' if safe_mode else 'safe off'} · {'manual approval' if not auto_mode else 'auto approval'}[/]"),
594
+ ("Memory", f"[text]{memory_count} saved · {'summary active' if summary_active else 'live context'}[/]"),
595
+ ("Theme", f"[secondary]{theme_name}[/]"),
596
+ ]
597
+ )
598
+ else:
599
+ header_status = Table.grid(expand=True)
600
+ header_status.add_column(ratio=2)
601
+ header_status.add_column(ratio=3)
602
+ header_status.add_column(ratio=2)
603
+ header_status.add_row(
604
+ Panel(
605
+ _kv_table(
606
+ [
607
+ ("Connected", f"[text]{execution_label}[/]"),
608
+ ("Session", f"[text]{model}[/]"),
609
+ ]
610
+ ),
611
+ border_style="border",
612
+ box=box.ROUNDED,
613
+ ),
614
+ Panel(
615
+ _kv_table(
616
+ [
617
+ ("Mode", f"[info]{mode_label}[/]"),
618
+ ("Context", f"[text]{context_line}[/]"),
619
+ ("Theme", f"[secondary]{theme_name}[/]"),
620
+ ]
621
+ ),
622
+ border_style="border",
623
+ box=box.ROUNDED,
624
+ ),
625
+ Panel(
626
+ _kv_table(
627
+ [
628
+ ("Auto", f"[text]{'on' if auto_mode else 'off'}[/]"),
629
+ ("Safe", f"[text]{'on' if safe_mode else 'off'}[/]"),
630
+ ("Memories", f"[text]{memory_count}[/]"),
631
+ ]
632
+ ),
633
+ border_style="border",
634
+ box=box.ROUNDED,
635
+ ),
636
+ )
637
+ header = Panel(
638
+ Group(
639
+ Text("Runtime Overview", style="bold primary"),
640
+ Text("Model, context, safety, and agent activity at a glance.", style="muted"),
641
+ header_status,
642
+ ),
643
+ border_style="border_accent",
644
+ box=box.ROUNDED,
645
+ padding=(1, 2),
646
+ )
647
+
648
+ chat_cfg = type(
649
+ "_ChatCfg",
650
+ (),
651
+ {
652
+ "messages": messages or [],
653
+ "temperature": temperature,
654
+ "num_ctx": total_tokens if total_tokens > 0 else None,
655
+ },
656
+ )()
657
+ inspector_cfg = type(
658
+ "_InspectorCfg",
659
+ (),
660
+ {
661
+ "model": model,
662
+ "cloud": cloud,
663
+ "system": system_prompt or "",
664
+ "temperature": temperature,
665
+ "num_ctx": total_tokens,
666
+ "tool_think_every": tool_think_every,
667
+ "cwd": cwd,
668
+ "host": host,
669
+ "session_summary": "active" if summary_active else "",
670
+ },
671
+ )()
672
+
673
+ left = _render_chat_panel(chat_cfg)
674
+ inspector = _render_inspector_panel(
675
+ inspector_cfg,
676
+ installed_models=installed_models,
677
+ running_models=running_models,
678
+ event_lines=event_lines or [
679
+ f"mode {mode_label}",
680
+ f"context {context_line}",
681
+ f"tool max {max_tool_iterations}",
682
+ f"reflect every {tool_think_every} calls",
683
+ ],
684
+ )
685
+
686
+ layout = Table.grid(expand=True)
687
+ if console.width < 110:
688
+ layout.add_column(ratio=1)
689
+ layout.add_row(left)
690
+ layout.add_row(inspector)
691
+ else:
692
+ layout.add_column(ratio=3)
693
+ layout.add_column(ratio=2)
694
+ layout.add_row(left, inspector)
695
+
696
+ console.print(header)
697
+ console.print(layout)
698
+ console.print(
699
+ Panel(
700
+ "[bold primary]Work[/]: /agent TASK /route TASK /changes /diff\n"
701
+ "[bold primary]Context[/]: /status /context /harness /hsearch /memories\n"
702
+ "[bold primary]Safety[/]: /doctor /safe /auto /policy\n"
703
+ "[bold primary]Setup[/]: /model /models /theme /reload /actions",
704
+ title="Quick Actions",
705
+ border_style="border",
706
+ box=box.ROUNDED,
707
+ )
708
+ )
709
+ console.print()
710
+
711
+
712
+ def _short_value(value: Any, limit: int = 80) -> str:
713
+ text = str(value)
714
+ text = " ".join(text.split())
715
+ return text if len(text) <= limit else text[: limit - 1] + "..."
716
+
717
+
718
+ def redact_tool_args(name: str, args: dict[str, Any]) -> dict[str, Any]:
719
+ """Remove secret-bearing values before display, JSON events, or audit previews."""
720
+
721
+ sensitive_keys = {"api_key", "access_token", "refresh_token", "password", "secret", "token"}
722
+ if name == "credential_helpers_store":
723
+ sensitive_keys.add("value")
724
+ return {
725
+ key: "<redacted>" if key.lower() in sensitive_keys else value
726
+ for key, value in args.items()
727
+ }
728
+
729
+
730
+ def show_tool_call(name: str, args: dict, *, call_id: str | None = None) -> None:
731
+ visible_args = redact_tool_args(
732
+ name,
733
+ {key: value for key, value in args.items() if key not in {"cwd", "safe_mode"}},
734
+ )
735
+ if _json_sink is not None:
736
+ cid = call_id or _json_sink.next_call_id()
737
+ _json_sink.tool_call(call_id=cid, name=name, args=visible_args)
738
+ return
739
+ rendered = " ".join(f"[secondary]{key}[/]={_short_value(value)}" for key, value in visible_args.items())
740
+ suffix = f" {rendered}" if rendered else ""
741
+ # DOING glyph (bolt) marks tool dispatch — matches the buddy's strike animation.
742
+ console.print(f"[accent]{glyph(AIState.DOING).plain}[/] [bold]{name}[/]{suffix}", highlight=False)
743
+
744
+
745
+ def show_tool_result(
746
+ name: str,
747
+ result: str,
748
+ approved: bool = True,
749
+ duration_ms: float | None = None,
750
+ *,
751
+ call_id: str | None = None,
752
+ ) -> None:
753
+ if _json_sink is not None:
754
+ if not approved and str(result).strip().lower().startswith("user denied"):
755
+ _json_sink.tool_denied(call_id=call_id, name=name, reason="approval-mode never; tool requires approval")
756
+ else:
757
+ _json_sink.tool_result(call_id=call_id, name=name, result=result, duration_ms=duration_ms)
758
+ return
759
+ status = "[success]OK[/]" if approved else "[error]ERR[/]"
760
+ lines = str(result).splitlines()
761
+ byte_count = len(str(result).encode("utf-8", errors="replace"))
762
+ duration = f" {duration_ms:.0f}ms" if duration_ms is not None else ""
763
+ console.print(f"{status} [bold]{name}[/]{duration} {_format_bytes(byte_count)} {len(lines)} lines")
764
+ preview = lines[:5] or [str(result)[:160]]
765
+ for line in preview:
766
+ console.print(f" [muted]{_short_value(line, 180)}[/]", highlight=False)
767
+ if len(lines) > 5:
768
+ console.print(f" [muted]... {len(lines) - 5} more lines[/]")
769
+
770
+
771
+ def show_recalled_context(blocks: list[dict[str, Any]]) -> None:
772
+ if not blocks:
773
+ return
774
+ if _json_sink is not None:
775
+ # Internal RAG detail — bridge consumers don't need this in the event stream.
776
+ return
777
+ lines: list[str] = []
778
+ for block in blocks[:5]:
779
+ block_type = str(block.get("type", "note")).upper()
780
+ block_id = str(block.get("id", "?"))
781
+ score = float(block.get("score", 0.0) or 0.0)
782
+ content = _short_value(block.get("content", ""), 180)
783
+ lines.append(f"[bold secondary][{block_type}][/bold secondary] [muted]{block_id}[/] [info]({score:.2f})[/]")
784
+ if content:
785
+ lines.append(f" [text]{content}[/]")
786
+ if len(blocks) > 5:
787
+ lines.append(f"[muted]... {len(blocks) - 5} more recalled block(s)[/]")
788
+ console.print(
789
+ Panel(
790
+ "\n".join(lines),
791
+ title=f"Recalled - {len(blocks)} block{'s' if len(blocks) != 1 else ''}",
792
+ border_style="border",
793
+ box=box.ROUNDED,
794
+ )
795
+ )
796
+
797
+
798
+ _DEFAULT_AGENT_PREVIEW_CHARS = 900
799
+ _AGENT_SECTION_HEADER = re.compile(
800
+ r"^(?:#{1,3}\s*)?"
801
+ r"(Assumptions|Risks|Concrete\s+Next\s+Steps|Next\s+Steps)"
802
+ r"\s*:?\s*$",
803
+ re.IGNORECASE,
804
+ )
805
+ _AGENT_BLOCK_OUTPUT_HEADER = re.compile(r"^#+\s*block\s+output\s*$", re.IGNORECASE)
806
+
807
+
808
+ def _agent_preview_char_limit() -> int | None:
809
+ """None means show full output (ALGO_CLI_AGENT_PREVIEW=0 or 'full')."""
810
+ raw = os.environ.get("ALGO_CLI_AGENT_PREVIEW", "").strip().lower()
811
+ if raw in {"", "default"}:
812
+ return _DEFAULT_AGENT_PREVIEW_CHARS
813
+ if raw in {"0", "full", "none", "off"}:
814
+ return None
815
+ try:
816
+ value = int(raw)
817
+ return None if value <= 0 else value
818
+ except ValueError:
819
+ return _DEFAULT_AGENT_PREVIEW_CHARS
820
+
821
+
822
+ def _agent_block_border_style(status: str) -> str:
823
+ normalized = (status or "").strip().lower()
824
+ if normalized == "complete":
825
+ return "success"
826
+ if normalized == "partial":
827
+ return "warning"
828
+ if normalized in {"failed", "error", "cancelled"}:
829
+ return "error"
830
+ return "border"
831
+
832
+
833
+ def _strip_block_output_header(text: str) -> str:
834
+ lines = text.splitlines()
835
+ if lines and _AGENT_BLOCK_OUTPUT_HEADER.match(lines[0].strip()):
836
+ return "\n".join(lines[1:]).strip()
837
+ return text.strip()
838
+
839
+
840
+ def _lines_to_bullets(lines: list[str]) -> list[str]:
841
+ bullets: list[str] = []
842
+ for line in lines:
843
+ stripped = line.strip()
844
+ if not stripped:
845
+ continue
846
+ stripped = re.sub(r"^[-*•]\s+", "", stripped)
847
+ stripped = re.sub(r"^\d+[.)]\s+", "", stripped)
848
+ if stripped:
849
+ bullets.append(stripped)
850
+ return bullets
851
+
852
+
853
+ def _parse_block_output_sections(text: str) -> dict[str, list[str]]:
854
+ """Parse Assumptions / Risks / Next Steps style sections into bullet lists."""
855
+ body = _strip_block_output_header(text)
856
+ if not body:
857
+ return {}
858
+ sections: dict[str, list[str]] = {}
859
+ current_label = ""
860
+ buffer: list[str] = []
861
+
862
+ def flush() -> None:
863
+ nonlocal buffer, current_label
864
+ if not current_label:
865
+ return
866
+ bullets = _lines_to_bullets(buffer)
867
+ if bullets:
868
+ key = current_label
869
+ if key in sections:
870
+ sections[key].extend(bullets)
871
+ else:
872
+ sections[key] = bullets
873
+ buffer = []
874
+
875
+ for line in body.splitlines():
876
+ header_match = _AGENT_SECTION_HEADER.match(line.strip())
877
+ if header_match:
878
+ flush()
879
+ label = header_match.group(1).lower()
880
+ if "concrete" in label or label == "next steps":
881
+ current_label = "Next steps"
882
+ elif label.startswith("risk"):
883
+ current_label = "Risks"
884
+ else:
885
+ current_label = "Assumptions"
886
+ continue
887
+ if current_label:
888
+ buffer.append(line)
889
+ else:
890
+ buffer.append(line)
891
+
892
+ flush()
893
+ if not sections and buffer:
894
+ lead = _lines_to_bullets(buffer)
895
+ if lead:
896
+ sections["Summary"] = lead[:12]
897
+ return sections
898
+
899
+
900
+ def _write_agent_block_dump(role: str, output: str) -> Path | None:
901
+ text = (output or "").strip()
902
+ if not text:
903
+ return None
904
+ safe_role = re.sub(r"[^a-zA-Z0-9_-]+", "-", (role or "block").strip()) or "block"
905
+ try:
906
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
907
+ path = CONFIG_DIR / f"last-block-{safe_role}.md"
908
+ text_with_eol = text + ("\n" if not text.endswith("\n") else "")
909
+ if _atomic_write_text is not None:
910
+ _atomic_write_text(path, text_with_eol)
911
+ else:
912
+ path.write_text(text_with_eol, encoding="utf-8")
913
+ return path
914
+ except OSError:
915
+ return None
916
+
917
+
918
+ def _agent_block_title(
919
+ role: str,
920
+ *,
921
+ duration_ms: float,
922
+ tool_calls: int,
923
+ status: str,
924
+ status_code: str = "",
925
+ model: str = "",
926
+ write_count: int = 0,
927
+ ) -> str:
928
+ duration_s = duration_ms / 1000.0
929
+ calls_label = "tool call" if tool_calls == 1 else "tool calls"
930
+ parts = [f"Agent · {role}", f"{duration_s:.1f}s", f"{tool_calls} {calls_label}", status]
931
+ if status_code:
932
+ parts.insert(-1, status_code)
933
+ if model:
934
+ parts.append(model)
935
+ if write_count > 0:
936
+ parts.append(f"writes:{write_count}")
937
+ return " — ".join(parts)
938
+
939
+
940
+ def _render_structured_sections(sections: dict[str, list[str]]) -> Group:
941
+ blocks: list[Any] = []
942
+ for title, bullets in sections.items():
943
+ blocks.append(Text(title, style="bold primary"))
944
+ for item in bullets[:16]:
945
+ blocks.append(Text(f" • {item}", style="text"))
946
+ if len(bullets) > 16:
947
+ blocks.append(Text(f" [muted]… {len(bullets) - 16} more[/]", style="muted"))
948
+ blocks.append(Text(""))
949
+ return Group(*blocks)
950
+
951
+
952
+ def _render_agent_block_body(
953
+ role: str,
954
+ output: str,
955
+ *,
956
+ preview_limit: int | None,
957
+ ) -> tuple[Any, Path | None]:
958
+ text = (output or "").strip() or "(no output produced)"
959
+ dump_path = _write_agent_block_dump(role, text if text != "(no output produced)" else "")
960
+
961
+ sections = _parse_block_output_sections(text)
962
+ use_structured = role == "plan" or len(sections) >= 2
963
+
964
+ if use_structured and sections:
965
+ body: Any = _render_structured_sections(sections)
966
+ remainder = _strip_block_output_header(text)
967
+ if preview_limit is not None and len(remainder) > preview_limit:
968
+ body = Group(
969
+ body,
970
+ Text(
971
+ f"[muted]Full text ({len(remainder)} chars) — see file or raise ALGO_CLI_AGENT_PREVIEW[/]",
972
+ style="muted",
973
+ ),
974
+ )
975
+ return body, dump_path
976
+
977
+ if preview_limit is None:
978
+ return Markdown(text), dump_path
979
+ if len(text) <= preview_limit:
980
+ return Markdown(text), dump_path
981
+ preview = _short_value(text, preview_limit)
982
+ return (
983
+ Group(
984
+ Markdown(preview),
985
+ Text(
986
+ f"[muted]Preview ({preview_limit} chars). Set ALGO_CLI_AGENT_PREVIEW=0 for full panel text.[/]",
987
+ style="muted",
988
+ ),
989
+ ),
990
+ dump_path,
991
+ )
992
+
993
+
994
+ def show_agent_block_start(
995
+ role: str,
996
+ model: str,
997
+ tool_count: int,
998
+ policy_summary: str = "",
999
+ *,
1000
+ policy_enforced: bool = False,
1001
+ cwd: str = "",
1002
+ ) -> None:
1003
+ if _json_sink is not None:
1004
+ return
1005
+ label = "Enforced policy" if policy_enforced else "Advisory policy"
1006
+ policy_line = f"\n[text]{label}:[/] {policy_summary}" if policy_summary else ""
1007
+ cwd_line = f"\n[text]Workspace:[/] [muted]{compact_path(cwd, 72)}[/]" if cwd else ""
1008
+ console.print(
1009
+ Panel(
1010
+ f"[text]Model:[/] [primary]{model}[/]\n[text]Runtime tools:[/] {tool_count}{cwd_line}{policy_line}",
1011
+ title=f"Agent · {role}",
1012
+ border_style="border_accent",
1013
+ box=box.ROUNDED,
1014
+ )
1015
+ )
1016
+
1017
+
1018
+ def show_agent_block_complete(
1019
+ role: str,
1020
+ output: str,
1021
+ *,
1022
+ duration_ms: float,
1023
+ tool_calls: int,
1024
+ status: str,
1025
+ status_reason: str = "",
1026
+ verification_warning: str = "",
1027
+ status_code: str = "",
1028
+ model: str = "",
1029
+ policy_summary: str = "",
1030
+ successful_writes: list[str] | None = None,
1031
+ ) -> None:
1032
+ if _json_sink is not None:
1033
+ return
1034
+ preview_limit = _agent_preview_char_limit()
1035
+ body_renderable, dump_path = _render_agent_block_body(
1036
+ role, output, preview_limit=preview_limit
1037
+ )
1038
+ writes = successful_writes or []
1039
+ write_count = len(writes)
1040
+
1041
+ header_lines: list[str] = []
1042
+ status_style = _agent_block_border_style(status)
1043
+ header_lines.append(f"[{status_style}]Status: {status.upper()}[/]")
1044
+ if status_code:
1045
+ header_lines.append(f"[muted]Code:[/] {status_code}")
1046
+ if status_reason:
1047
+ header_lines.append(f"[text]{_short_value(status_reason, 220)}[/]")
1048
+ if verification_warning and not status_reason:
1049
+ header_lines.append(
1050
+ f"[warning]Verification warning:[/] {_short_value(verification_warning, 200)}"
1051
+ )
1052
+
1053
+ footer_parts = [
1054
+ f"[muted]{tool_calls} tool call{'s' if tool_calls != 1 else ''}[/]",
1055
+ f"[muted]{duration_ms / 1000:.1f}s[/]",
1056
+ ]
1057
+ if policy_summary:
1058
+ footer_parts.append(f"[muted]{_short_value(policy_summary, 120)}[/]")
1059
+ if dump_path is not None:
1060
+ footer_parts.append(f"[info]Full output:[/] [accent]{dump_path}[/]")
1061
+ if write_count:
1062
+ footer_parts.append(f"[success]writes: {write_count}[/]")
1063
+
1064
+ header_renderable = (
1065
+ Text.from_markup("\n".join(header_lines)) if header_lines else Text("")
1066
+ )
1067
+ footer_renderable = Text.from_markup(" · ".join(footer_parts)) if footer_parts else Text("")
1068
+ panel_body = Group(
1069
+ header_renderable,
1070
+ Text(""),
1071
+ body_renderable,
1072
+ Text(""),
1073
+ footer_renderable,
1074
+ )
1075
+ if verification_warning and status_reason:
1076
+ panel_body = Group(
1077
+ panel_body,
1078
+ Text(""),
1079
+ Text.from_markup(
1080
+ f"[warning]Verification:[/] {_short_value(verification_warning, 200)}"
1081
+ ),
1082
+ )
1083
+
1084
+ console.print(
1085
+ Panel(
1086
+ panel_body,
1087
+ title=_agent_block_title(
1088
+ role,
1089
+ duration_ms=duration_ms,
1090
+ tool_calls=tool_calls,
1091
+ status=status,
1092
+ status_code=status_code,
1093
+ model=model,
1094
+ write_count=write_count,
1095
+ ),
1096
+ border_style=status_style,
1097
+ box=box.ROUNDED,
1098
+ )
1099
+ )
1100
+
1101
+
1102
+ def show_agent_recovery_start(role: str, reason: str, retry_iterations: int) -> None:
1103
+ console.print(
1104
+ Panel(
1105
+ Markdown(
1106
+ f"**Reason:** {reason}\n\n"
1107
+ f"**Recovery:** one tool-free replan, then one focused retry "
1108
+ f"with at most {retry_iterations} iterations."
1109
+ ),
1110
+ title=f"Recovery - {role} retry",
1111
+ border_style="warning",
1112
+ box=box.ROUNDED,
1113
+ )
1114
+ )
1115
+
1116
+
1117
+ def show_agent_pipeline_complete(output: str, *, block_count: int, duration_ms: float) -> None:
1118
+ console.print(
1119
+ Panel(
1120
+ Markdown(output.strip() or "(no final output produced)"),
1121
+ title=f"Pipeline complete - {block_count} blocks - {duration_ms / 1000:.1f}s",
1122
+ border_style="success",
1123
+ box=box.ROUNDED,
1124
+ )
1125
+ )
1126
+
1127
+
1128
+ def _estimated_tokens(text: str) -> int:
1129
+ return max(1, len(text) // 4) if text else 0
1130
+
1131
+
1132
+ def _thinking_renderable(text: str, *, final: bool = False) -> Panel:
1133
+ visible = text[:_THINKING_VISIBLE_CHARS]
1134
+ token_count = _estimated_tokens(text)
1135
+ truncated = len(text) > _THINKING_VISIBLE_CHARS
1136
+ if truncated:
1137
+ visible = visible.rstrip() + f"\n\n[muted]... truncated, ~{token_count} tokens total[/]"
1138
+ elapsed = time.monotonic() - _thinking_started_at if _thinking_started_at else 0.0
1139
+ # Non-final title carries a time-derived frame: each Live update (token
1140
+ # batches arrive faster than the 90ms frame interval) advances the pulse,
1141
+ # so the panel animates without any extra timer.
1142
+ title = (
1143
+ f"Thinking - {elapsed:.1f}s - ~{token_count} tokens"
1144
+ if final
1145
+ else f"{current_frame(AIState.THINKING)} Thinking..."
1146
+ )
1147
+ return Panel(
1148
+ Text(visible or "Thinking...", style="secondary italic"),
1149
+ title=title,
1150
+ border_style="border",
1151
+ box=box.ROUNDED,
1152
+ )
1153
+
1154
+
1155
+ def start_thinking_block() -> None:
1156
+ global _thinking_live, _thinking_buffer, _thinking_pending, _thinking_last_render_time, _thinking_started_at
1157
+ if _json_sink is not None:
1158
+ return
1159
+ if _thinking_live is not None:
1160
+ return
1161
+ finish_streaming_response()
1162
+ _thinking_buffer = ""
1163
+ _thinking_pending = ""
1164
+ _thinking_last_render_time = 0.0
1165
+ _thinking_started_at = time.monotonic()
1166
+ _thinking_live = Live(_thinking_renderable(""), console=console, auto_refresh=False, transient=False)
1167
+ _thinking_live.start()
1168
+ _thinking_live.refresh()
1169
+
1170
+
1171
+ def show_thinking_text(text: str) -> None:
1172
+ global _thinking_buffer, _thinking_pending, _thinking_last_render_time
1173
+ if not text:
1174
+ return
1175
+ if _json_sink is not None:
1176
+ _json_sink.thinking(text)
1177
+ return
1178
+ if _thinking_live is None:
1179
+ start_thinking_block()
1180
+ _thinking_pending += text
1181
+ now = time.monotonic()
1182
+ if _thinking_live is not None and now - _thinking_last_render_time >= _RENDER_INTERVAL:
1183
+ _thinking_buffer += _thinking_pending
1184
+ _thinking_pending = ""
1185
+ _thinking_last_render_time = now
1186
+ _thinking_live.update(_thinking_renderable(_thinking_buffer), refresh=True)
1187
+
1188
+
1189
+ def finish_thinking_block() -> None:
1190
+ global _thinking_live, _thinking_buffer, _thinking_pending, _thinking_started_at
1191
+ if _json_sink is not None:
1192
+ return
1193
+ if _thinking_live is not None:
1194
+ if _thinking_pending:
1195
+ _thinking_buffer += _thinking_pending
1196
+ _thinking_pending = ""
1197
+ _thinking_live.update(_thinking_renderable(_thinking_buffer, final=True), refresh=True)
1198
+ _thinking_live.stop()
1199
+ _thinking_live = None
1200
+ _thinking_buffer = ""
1201
+ _thinking_started_at = 0.0
1202
+
1203
+
1204
+ def show_response(text: str) -> None:
1205
+ if _json_sink is not None:
1206
+ _json_sink.content(text)
1207
+ return
1208
+ if text.strip():
1209
+ console.print(Markdown(text))
1210
+ console.print()
1211
+
1212
+
1213
+ def start_streaming_response() -> None:
1214
+ global _stream_live, _stream_buffer, _stream_pending, _last_render_time
1215
+ if _json_sink is not None:
1216
+ return
1217
+ finish_streaming_response()
1218
+ # Answering transition: a left-anchored eye+chevron rule separates the
1219
+ # response from tool chatter, so the state flip reads instantly.
1220
+ rule_title = Text.assemble(
1221
+ glyph(AIState.IDLE), (" ", ""), glyph(AIState.ANSWERING), (" answering", "primary")
1222
+ )
1223
+ console.rule(rule_title, style="border", align="left")
1224
+ _stream_buffer = ""
1225
+ _stream_pending = ""
1226
+ _last_render_time = 0.0
1227
+ _stream_live = Live(Markdown(""), console=console, auto_refresh=False, transient=False)
1228
+ _stream_live.start()
1229
+
1230
+
1231
+ def show_stream_text(text: str) -> None:
1232
+ """Accumulate tokens and flush to the Live display at most 12 times per second.
1233
+
1234
+ Calling Markdown() on every token is O(n²) — the whole buffer is re-parsed
1235
+ each time. This coalescing buffer ensures Markdown() is called at most
1236
+ refresh_per_second times regardless of how fast tokens arrive.
1237
+ """
1238
+ global _stream_buffer, _stream_pending, _last_render_time
1239
+ if _json_sink is not None:
1240
+ _json_sink.content(text)
1241
+ return
1242
+ if _stream_live is None:
1243
+ console.print(text, end="", highlight=False)
1244
+ return
1245
+ _stream_pending += text
1246
+ now = time.monotonic()
1247
+ if now - _last_render_time >= _RENDER_INTERVAL:
1248
+ _stream_buffer += _stream_pending
1249
+ _stream_pending = ""
1250
+ _last_render_time = now
1251
+ _stream_live.update(Markdown(_stream_buffer), refresh=True)
1252
+
1253
+
1254
+ def finish_streaming_response() -> None:
1255
+ global _stream_live, _stream_buffer, _stream_pending
1256
+ if _json_sink is not None:
1257
+ return
1258
+ if _stream_live is not None:
1259
+ if _stream_pending:
1260
+ _stream_buffer += _stream_pending
1261
+ _stream_pending = ""
1262
+ _stream_live.update(Markdown(_stream_buffer or ""), refresh=True)
1263
+ _stream_live.stop()
1264
+ _stream_live = None
1265
+ _stream_buffer = ""
1266
+
1267
+
1268
+ def show_thinking_token(text: str) -> None:
1269
+ show_thinking_text(text)
1270
+
1271
+
1272
+ def show_error(msg: str) -> None:
1273
+ if _json_sink is not None and not _console_capture_active.get():
1274
+ _json_sink.error(error_class="internal", message=msg)
1275
+ return
1276
+ marker = Text.assemble(glyph(AIState.ERROR), (" Error: ", "bold error"))
1277
+ console.print(marker.append(msg, style="text"))
1278
+
1279
+
1280
+ def show_info(msg: str) -> None:
1281
+ if _json_sink is not None and not _console_capture_active.get():
1282
+ # info chatter is dropped in JSON mode; bridge consumers only want events.
1283
+ return
1284
+ console.print(f"[info]{msg}[/]")
1285
+
1286
+
1287
+ def show_status_footer(model: str, used_tokens: int, total_tokens: int, summary_active: bool = False) -> None:
1288
+ """Fallback status line when prompt_toolkit toolbar is unavailable."""
1289
+ if json_mode_active():
1290
+ return
1291
+ # The companion eye fronts the footer; frame is wall-clock derived so
1292
+ # repeated footer redraws make it breathe/blink.
1293
+ idle_glyph = buddy_frame(AIState.IDLE).plain
1294
+ if total_tokens <= 0:
1295
+ console.print(f"[info]{idle_glyph} {model} · ctx unknown[/]")
1296
+ return
1297
+ used = min(max(used_tokens, 0), total_tokens)
1298
+ remaining = max(total_tokens - used, 0)
1299
+ pct_left = int(round((remaining / total_tokens) * 100))
1300
+ ctx_color = "info" if pct_left >= 50 else ("warning" if pct_left >= 20 else "error")
1301
+ summary_flag = " · summary" if summary_active else ""
1302
+ console.print(
1303
+ f"[info]{idle_glyph}[/] [bold]{model}[/] · "
1304
+ f"[{ctx_color}]▣ {used}/{total_tokens} ({pct_left}% left)[/]{summary_flag}"
1305
+ )
1306
+
1307
+
1308
+ def show_memory(facts: list[str]) -> None:
1309
+ if not facts:
1310
+ console.print("[info]No memories stored.[/]")
1311
+ return
1312
+ table = Table(title="Memories", show_header=True, header_style="primary", box=box.ROUNDED)
1313
+ table.add_column("#", justify="right")
1314
+ table.add_column("Fact")
1315
+ for index, fact in enumerate(facts, 1):
1316
+ table.add_row(str(index), fact)
1317
+ console.print(table)
1318
+
1319
+
1320
+ def show_help() -> None:
1321
+ table = Table.grid(padding=(0, 2), expand=True)
1322
+ table.add_column(style="muted", no_wrap=True)
1323
+ table.add_column(style="primary", no_wrap=True)
1324
+ table.add_column(style="text")
1325
+
1326
+ from . import slash_dispatch
1327
+
1328
+ def _group(command: str) -> str:
1329
+ if command in {
1330
+ "/help",
1331
+ "/dashboard",
1332
+ "/status",
1333
+ "/actions",
1334
+ "/reload",
1335
+ "/theme",
1336
+ "/info",
1337
+ "/safe",
1338
+ "/auto",
1339
+ "/policy",
1340
+ "/thinking",
1341
+ "/verify",
1342
+ "/perf",
1343
+ "/metrics",
1344
+ "/doctor",
1345
+ "/selfcheck",
1346
+ "/exit",
1347
+ "/quit",
1348
+ }:
1349
+ return "Session"
1350
+ if command in {
1351
+ "/model",
1352
+ "/models",
1353
+ "/host",
1354
+ "/cloud",
1355
+ "/cloudauto",
1356
+ "/login",
1357
+ "/keepalive",
1358
+ "/ctx",
1359
+ "/temp",
1360
+ "/toolmax",
1361
+ "/thinkevery",
1362
+ "/system",
1363
+ }:
1364
+ return "Model"
1365
+ if command.startswith("/google"):
1366
+ return "Google"
1367
+ if command.startswith("/chatgpt"):
1368
+ return "ChatGPT"
1369
+ if command.startswith("/xai") or command.startswith("/x-account"):
1370
+ return "xAI"
1371
+ if (
1372
+ command.startswith("/harness")
1373
+ or command.startswith("/hsearch")
1374
+ or command.startswith("/hread")
1375
+ or command in {"/hs", "/hr"}
1376
+ ):
1377
+ return "Harness"
1378
+ if command in {
1379
+ "/reason",
1380
+ "/reflex",
1381
+ "/goal",
1382
+ "/agent",
1383
+ "/route",
1384
+ "/icl",
1385
+ "/model-check",
1386
+ "/context",
1387
+ }:
1388
+ return "Agent"
1389
+ if command.startswith("/kernel"):
1390
+ return "Knowledge"
1391
+ if command in {
1392
+ "/identity",
1393
+ "/lesson",
1394
+ "/lessons",
1395
+ "/skills",
1396
+ "/remember",
1397
+ "/memories",
1398
+ "/forget",
1399
+ "/intuition",
1400
+ "/intelligence",
1401
+ "/intel",
1402
+ "/intelagence",
1403
+ }:
1404
+ return "Knowledge"
1405
+ if command in {"/cd", "/ls", "/read", "/save", "/load", "/diff", "/changes", "/clear", "/mode"}:
1406
+ return "Workspace"
1407
+ if command in {"/embed", "/vision", "/pdf"}:
1408
+ return "Media"
1409
+ if command in {"/plugins", "/credentials", "/url-scheme"}:
1410
+ return "Integrations"
1411
+ return "Other"
1412
+
1413
+ groups: dict[str, list[tuple[str, str]]] = {
1414
+ "Session": [],
1415
+ "Model": [],
1416
+ "Workspace": [],
1417
+ "Knowledge": [],
1418
+ "Agent": [],
1419
+ "xAI": [],
1420
+ "Google": [],
1421
+ "ChatGPT": [],
1422
+ "Harness": [],
1423
+ "Media": [],
1424
+ "Integrations": [],
1425
+ "Other": [],
1426
+ }
1427
+ for command, description in slash_dispatch.SLASH_COMMANDS:
1428
+ groups[_group(command)].append((command, description))
1429
+
1430
+ for group in [
1431
+ "Session",
1432
+ "Model",
1433
+ "Workspace",
1434
+ "Knowledge",
1435
+ "Agent",
1436
+ "xAI",
1437
+ "Google",
1438
+ "ChatGPT",
1439
+ "Harness",
1440
+ "Media",
1441
+ "Integrations",
1442
+ "Other",
1443
+ ]:
1444
+ rows = groups[group]
1445
+ if not rows:
1446
+ continue
1447
+ for index, (command, description) in enumerate(rows):
1448
+ table.add_row(group if index == 0 else "", command, description)
1449
+ table.add_row("", "", "")
1450
+ intro = Text.assemble(
1451
+ ("Ask naturally for ordinary work. ", "text"),
1452
+ ("Slash commands control the runtime and session.\n", "muted"),
1453
+ ("Start here ", "bold secondary"),
1454
+ ("/status", "primary"),
1455
+ (" · ", "muted"),
1456
+ ("/agent TASK", "primary"),
1457
+ (" · ", "muted"),
1458
+ ("/hsearch QUERY", "primary"),
1459
+ (" · ", "muted"),
1460
+ ("/doctor", "primary"),
1461
+ )
1462
+ console.print(
1463
+ Panel(
1464
+ Group(intro, Text(""), table),
1465
+ title="Command Reference",
1466
+ subtitle="[muted]type / to search inline[/]",
1467
+ border_style="border_accent",
1468
+ box=box.ROUNDED,
1469
+ )
1470
+ )