tsugite-cli 0.3.3__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.
- tsugite/__init__.py +6 -0
- tsugite/agent_composition.py +163 -0
- tsugite/agent_inheritance.py +479 -0
- tsugite/agent_preparation.py +236 -0
- tsugite/agent_runner/__init__.py +45 -0
- tsugite/agent_runner/helpers.py +106 -0
- tsugite/agent_runner/history_integration.py +248 -0
- tsugite/agent_runner/metrics.py +100 -0
- tsugite/agent_runner/runner.py +1879 -0
- tsugite/agent_runner/validation.py +70 -0
- tsugite/agent_utils.py +167 -0
- tsugite/attachments/__init__.py +65 -0
- tsugite/attachments/auto_context.py +199 -0
- tsugite/attachments/base.py +34 -0
- tsugite/attachments/file.py +51 -0
- tsugite/attachments/inline.py +31 -0
- tsugite/attachments/storage.py +178 -0
- tsugite/attachments/url.py +59 -0
- tsugite/attachments/youtube.py +101 -0
- tsugite/benchmark/__init__.py +62 -0
- tsugite/benchmark/config.py +183 -0
- tsugite/benchmark/core.py +292 -0
- tsugite/benchmark/discovery.py +377 -0
- tsugite/benchmark/evaluators.py +671 -0
- tsugite/benchmark/execution.py +657 -0
- tsugite/benchmark/metrics.py +204 -0
- tsugite/benchmark/reports.py +420 -0
- tsugite/benchmark/utils.py +288 -0
- tsugite/builtin_agents/chat-assistant.md +53 -0
- tsugite/builtin_agents/default.md +140 -0
- tsugite/builtin_agents.py +5 -0
- tsugite/cache.py +195 -0
- tsugite/cli/__init__.py +1042 -0
- tsugite/cli/agents.py +148 -0
- tsugite/cli/attachments.py +193 -0
- tsugite/cli/benchmark.py +663 -0
- tsugite/cli/cache.py +113 -0
- tsugite/cli/config.py +272 -0
- tsugite/cli/helpers.py +534 -0
- tsugite/cli/history.py +193 -0
- tsugite/cli/init.py +387 -0
- tsugite/cli/mcp.py +193 -0
- tsugite/cli/tools.py +419 -0
- tsugite/config.py +204 -0
- tsugite/console.py +48 -0
- tsugite/constants.py +21 -0
- tsugite/core/__init__.py +19 -0
- tsugite/core/agent.py +774 -0
- tsugite/core/executor.py +300 -0
- tsugite/core/memory.py +67 -0
- tsugite/core/tools.py +271 -0
- tsugite/docker_cli.py +270 -0
- tsugite/events/__init__.py +55 -0
- tsugite/events/base.py +46 -0
- tsugite/events/bus.py +62 -0
- tsugite/events/events.py +224 -0
- tsugite/exceptions.py +40 -0
- tsugite/history/__init__.py +29 -0
- tsugite/history/index.py +210 -0
- tsugite/history/models.py +106 -0
- tsugite/history/storage.py +157 -0
- tsugite/mcp_client.py +219 -0
- tsugite/mcp_config.py +174 -0
- tsugite/md_agents.py +751 -0
- tsugite/models.py +257 -0
- tsugite/renderer.py +151 -0
- tsugite/shell_tool_config.py +265 -0
- tsugite/templates/assistant.md +14 -0
- tsugite/tools/__init__.py +265 -0
- tsugite/tools/agents.py +312 -0
- tsugite/tools/edit_strategies.py +393 -0
- tsugite/tools/fs.py +329 -0
- tsugite/tools/http.py +239 -0
- tsugite/tools/interactive.py +430 -0
- tsugite/tools/shell.py +129 -0
- tsugite/tools/shell_tools.py +214 -0
- tsugite/tools/tasks.py +339 -0
- tsugite/tsugite.py +7 -0
- tsugite/ui/__init__.py +46 -0
- tsugite/ui/base.py +638 -0
- tsugite/ui/chat.py +265 -0
- tsugite/ui/chat.tcss +92 -0
- tsugite/ui/chat_history.py +286 -0
- tsugite/ui/helpers.py +102 -0
- tsugite/ui/jsonl.py +125 -0
- tsugite/ui/live_template.py +529 -0
- tsugite/ui/plain.py +419 -0
- tsugite/ui/textual_chat.py +642 -0
- tsugite/ui/textual_handler.py +225 -0
- tsugite/ui/widgets/__init__.py +6 -0
- tsugite/ui/widgets/base_scroll_log.py +27 -0
- tsugite/ui/widgets/message_list.py +121 -0
- tsugite/ui/widgets/thought_log.py +80 -0
- tsugite/ui_context.py +90 -0
- tsugite/utils.py +367 -0
- tsugite/xdg.py +104 -0
- tsugite_cli-0.3.3.dist-info/METADATA +325 -0
- tsugite_cli-0.3.3.dist-info/RECORD +101 -0
- tsugite_cli-0.3.3.dist-info/WHEEL +4 -0
- tsugite_cli-0.3.3.dist-info/entry_points.txt +5 -0
- tsugite_cli-0.3.3.dist-info/licenses/LICENSE +235 -0
tsugite/ui/plain.py
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""Plain text UI handler without colors, panels, or emojis."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from typing import Any, Generator
|
|
6
|
+
|
|
7
|
+
from tsugite.console import get_stderr_console
|
|
8
|
+
from tsugite.events import (
|
|
9
|
+
CodeExecutionEvent,
|
|
10
|
+
CostSummaryEvent,
|
|
11
|
+
ErrorEvent,
|
|
12
|
+
ExecutionLogsEvent,
|
|
13
|
+
ExecutionResultEvent,
|
|
14
|
+
FinalAnswerEvent,
|
|
15
|
+
LLMMessageEvent,
|
|
16
|
+
ObservationEvent,
|
|
17
|
+
ReasoningContentEvent,
|
|
18
|
+
ReasoningTokensEvent,
|
|
19
|
+
StepStartEvent,
|
|
20
|
+
TaskStartEvent,
|
|
21
|
+
ToolCallEvent,
|
|
22
|
+
)
|
|
23
|
+
from tsugite.ui.base import CustomUIHandler
|
|
24
|
+
from tsugite.ui_context import clear_ui_context, set_ui_context
|
|
25
|
+
|
|
26
|
+
# Display constants for plain UI output
|
|
27
|
+
MAX_OBSERVATION_PREVIEW_LENGTH = 200 # Truncate long observations in plain output
|
|
28
|
+
MAX_REASONING_DISPLAY_LENGTH = 2000 # Limit reasoning content to keep output manageable
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PlainUIHandler(CustomUIHandler):
|
|
32
|
+
"""Plain text UI handler without colors, panels, animations, or emojis."""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
"""Initialize plain UI handler with no-color console."""
|
|
36
|
+
no_color_console = get_stderr_console(no_color=True)
|
|
37
|
+
|
|
38
|
+
# Initialize parent with panels disabled
|
|
39
|
+
super().__init__(
|
|
40
|
+
console=no_color_console,
|
|
41
|
+
show_code=True,
|
|
42
|
+
show_observations=True,
|
|
43
|
+
show_llm_messages=False,
|
|
44
|
+
show_execution_results=True,
|
|
45
|
+
show_execution_logs=True,
|
|
46
|
+
show_panels=False,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def _strip_emojis(text: str) -> str:
|
|
51
|
+
"""Remove all emojis from text.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
text: Text potentially containing emojis
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Text with emojis removed
|
|
58
|
+
"""
|
|
59
|
+
# Pattern matches emoji characters
|
|
60
|
+
emoji_pattern = re.compile(
|
|
61
|
+
"["
|
|
62
|
+
"\U0001f600-\U0001f64f" # emoticons
|
|
63
|
+
"\U0001f300-\U0001f5ff" # symbols & pictographs
|
|
64
|
+
"\U0001f680-\U0001f6ff" # transport & map symbols
|
|
65
|
+
"\U0001f1e0-\U0001f1ff" # flags
|
|
66
|
+
"\U00002702-\U000027b0"
|
|
67
|
+
"\U000024c2-\U0001f251"
|
|
68
|
+
"]+",
|
|
69
|
+
flags=re.UNICODE,
|
|
70
|
+
)
|
|
71
|
+
return emoji_pattern.sub("", text).strip()
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _strip_rich_markup(text: str) -> str:
|
|
75
|
+
"""Remove Rich markup tags from text.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
text: Text potentially containing Rich markup like [bold], [cyan], etc.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Text with Rich markup removed
|
|
82
|
+
"""
|
|
83
|
+
# Remove Rich color/style tags like [cyan], [/cyan], [bold], etc.
|
|
84
|
+
return re.sub(r"\[/?[a-z\s]+\]", "", text)
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _is_final_answer(text: str) -> bool:
|
|
88
|
+
"""Check if text contains a final answer marker.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
text: Text to check
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
True if text contains final answer marker
|
|
95
|
+
"""
|
|
96
|
+
return "__FINAL_ANSWER__:" in text
|
|
97
|
+
|
|
98
|
+
def _handle_task_start(self, event: TaskStartEvent) -> None:
|
|
99
|
+
"""Handle task start event with plain text output."""
|
|
100
|
+
self.state.task = event.task
|
|
101
|
+
self.state.current_step = 0
|
|
102
|
+
self.state.steps_history = []
|
|
103
|
+
|
|
104
|
+
# Show plain text task details (main banner already shown by CLI)
|
|
105
|
+
# Only show full prompt with --verbose (via show_debug_messages flag)
|
|
106
|
+
if self.show_debug_messages:
|
|
107
|
+
self.console.print(f"Task: {self.state.task}")
|
|
108
|
+
model = event.model
|
|
109
|
+
if model:
|
|
110
|
+
self.console.print(f"Model: {model}")
|
|
111
|
+
self.console.print()
|
|
112
|
+
|
|
113
|
+
def _handle_step_start(self, event: StepStartEvent) -> None:
|
|
114
|
+
"""Handle step start event with plain text output."""
|
|
115
|
+
self.state.current_step = event.step
|
|
116
|
+
|
|
117
|
+
# Show "Turn" for reasoning iterations
|
|
118
|
+
label = f"Turn {self.state.current_step}"
|
|
119
|
+
|
|
120
|
+
self.console.print(f"{label}: Waiting for LLM response...")
|
|
121
|
+
|
|
122
|
+
# Add step to history
|
|
123
|
+
self.state.steps_history.append({"step": self.state.current_step, "status": "in_progress", "actions": []})
|
|
124
|
+
|
|
125
|
+
def _handle_code_execution(self, event: CodeExecutionEvent) -> None:
|
|
126
|
+
"""Handle code execution event with plain text output."""
|
|
127
|
+
self.state.code_being_executed = event.code
|
|
128
|
+
|
|
129
|
+
self.console.print("Executing code...")
|
|
130
|
+
|
|
131
|
+
if self.show_code and self.state.code_being_executed:
|
|
132
|
+
self.console.print()
|
|
133
|
+
self.console.rule("Executing Code", style="dim")
|
|
134
|
+
self.console.print(self.state.code_being_executed)
|
|
135
|
+
self.console.rule(style="dim")
|
|
136
|
+
self.console.print()
|
|
137
|
+
|
|
138
|
+
def _handle_tool_call(self, event: ToolCallEvent) -> None:
|
|
139
|
+
"""Handle tool call event with plain text output."""
|
|
140
|
+
content = event.tool
|
|
141
|
+
|
|
142
|
+
self.console.print("Calling tool...")
|
|
143
|
+
|
|
144
|
+
# Add to current step history
|
|
145
|
+
if self.state.steps_history:
|
|
146
|
+
self.state.steps_history[-1]["actions"].append({"type": "tool_call", "content": content})
|
|
147
|
+
|
|
148
|
+
def _handle_observation(self, event: ObservationEvent) -> None:
|
|
149
|
+
"""Handle observation event with plain text output."""
|
|
150
|
+
observation = event.observation
|
|
151
|
+
|
|
152
|
+
self.console.print("Processing results...")
|
|
153
|
+
|
|
154
|
+
if observation:
|
|
155
|
+
# Clean up observation for display
|
|
156
|
+
clean_obs = observation.replace("|", "[").strip()
|
|
157
|
+
|
|
158
|
+
# Check if this is a final answer or error
|
|
159
|
+
is_final_answer = self._is_final_answer(clean_obs)
|
|
160
|
+
is_error = self._contains_error(clean_obs)
|
|
161
|
+
|
|
162
|
+
# Always show errors, even if show_observations is False
|
|
163
|
+
# Skip final answers here - they will be displayed by _handle_final_answer event
|
|
164
|
+
if is_error:
|
|
165
|
+
# Display errors prominently
|
|
166
|
+
self.console.print()
|
|
167
|
+
self.console.rule("ERROR", style="dim")
|
|
168
|
+
self.console.print(clean_obs)
|
|
169
|
+
self.console.rule(style="dim")
|
|
170
|
+
self.console.print()
|
|
171
|
+
elif is_final_answer:
|
|
172
|
+
# Skip displaying final answer here - it will be displayed by _handle_final_answer event
|
|
173
|
+
pass
|
|
174
|
+
elif self.show_observations:
|
|
175
|
+
# Normal observation - truncate if too long
|
|
176
|
+
if len(clean_obs) > MAX_OBSERVATION_PREVIEW_LENGTH:
|
|
177
|
+
clean_obs = clean_obs[:MAX_OBSERVATION_PREVIEW_LENGTH] + "..."
|
|
178
|
+
self.console.print(f"Result: {clean_obs}")
|
|
179
|
+
|
|
180
|
+
# Add to current step history
|
|
181
|
+
if self.state.steps_history:
|
|
182
|
+
self.state.steps_history[-1]["actions"].append({"type": "observation", "content": observation})
|
|
183
|
+
self.state.steps_history[-1]["status"] = "completed"
|
|
184
|
+
|
|
185
|
+
def _handle_final_answer(self, event: FinalAnswerEvent) -> None:
|
|
186
|
+
"""Handle final answer event with plain text output."""
|
|
187
|
+
answer = str(event.answer)
|
|
188
|
+
|
|
189
|
+
self.console.print("Finalizing answer...")
|
|
190
|
+
|
|
191
|
+
# Render final answer as markdown
|
|
192
|
+
from rich.markdown import Markdown
|
|
193
|
+
|
|
194
|
+
self.console.print()
|
|
195
|
+
self.console.rule("FINAL ANSWER")
|
|
196
|
+
self.console.print(Markdown(answer))
|
|
197
|
+
self.console.rule()
|
|
198
|
+
self.console.print()
|
|
199
|
+
|
|
200
|
+
def _handle_error(self, event: ErrorEvent) -> None:
|
|
201
|
+
"""Handle error event with plain text output."""
|
|
202
|
+
error = event.error
|
|
203
|
+
error_type = event.error_type or "Error"
|
|
204
|
+
|
|
205
|
+
self.console.print("Error occurred...")
|
|
206
|
+
|
|
207
|
+
# Always show errors prominently
|
|
208
|
+
self.console.print()
|
|
209
|
+
self.console.rule(f"{error_type}")
|
|
210
|
+
self.console.print(error)
|
|
211
|
+
self.console.rule()
|
|
212
|
+
self.console.print()
|
|
213
|
+
|
|
214
|
+
# Add to current step history
|
|
215
|
+
if self.state.steps_history:
|
|
216
|
+
self.state.steps_history[-1]["actions"].append({"type": "error", "content": error})
|
|
217
|
+
self.state.steps_history[-1]["status"] = "error"
|
|
218
|
+
|
|
219
|
+
def _handle_llm_message(self, event: LLMMessageEvent) -> None:
|
|
220
|
+
"""Handle LLM reasoning message event with plain text output."""
|
|
221
|
+
if not self.show_llm_messages:
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
content = event.content
|
|
225
|
+
title = event.title or "Agent Reasoning"
|
|
226
|
+
|
|
227
|
+
if content.strip():
|
|
228
|
+
self.console.print()
|
|
229
|
+
self.console.rule(title, style="dim")
|
|
230
|
+
self.console.print(content.strip())
|
|
231
|
+
self.console.rule(style="dim")
|
|
232
|
+
self.console.print()
|
|
233
|
+
|
|
234
|
+
def _handle_reasoning_content(self, event: ReasoningContentEvent) -> None:
|
|
235
|
+
"""Handle reasoning content with plain text output."""
|
|
236
|
+
content = event.content
|
|
237
|
+
step = event.step
|
|
238
|
+
|
|
239
|
+
if content and content.strip():
|
|
240
|
+
# Build title with turn number if available
|
|
241
|
+
if step is not None:
|
|
242
|
+
title = f"Model Reasoning (Turn {step})"
|
|
243
|
+
else:
|
|
244
|
+
title = "Model Reasoning"
|
|
245
|
+
|
|
246
|
+
self.console.print("Processing reasoning content...")
|
|
247
|
+
|
|
248
|
+
# Truncate very long reasoning content for display
|
|
249
|
+
display_content = content.strip()
|
|
250
|
+
if len(display_content) > MAX_REASONING_DISPLAY_LENGTH:
|
|
251
|
+
display_content = display_content[:MAX_REASONING_DISPLAY_LENGTH] + "\n\n... (truncated)"
|
|
252
|
+
|
|
253
|
+
self.console.print()
|
|
254
|
+
self.console.rule(title, style="dim")
|
|
255
|
+
self.console.print(display_content)
|
|
256
|
+
self.console.rule(style="dim")
|
|
257
|
+
self.console.print()
|
|
258
|
+
|
|
259
|
+
def _handle_reasoning_tokens(self, event: ReasoningTokensEvent) -> None:
|
|
260
|
+
"""Handle reasoning token counts with plain text output."""
|
|
261
|
+
tokens = event.tokens
|
|
262
|
+
step = event.step
|
|
263
|
+
|
|
264
|
+
if tokens:
|
|
265
|
+
# Build message with turn number if available
|
|
266
|
+
if step is not None:
|
|
267
|
+
message = f"Turn {step}: Used {tokens} reasoning tokens"
|
|
268
|
+
else:
|
|
269
|
+
message = f"Used {tokens} reasoning tokens"
|
|
270
|
+
|
|
271
|
+
self.console.print(message)
|
|
272
|
+
|
|
273
|
+
def _handle_cost_summary(self, event: CostSummaryEvent) -> None:
|
|
274
|
+
"""Handle cost summary display after final answer."""
|
|
275
|
+
cost = event.cost
|
|
276
|
+
tokens = event.tokens
|
|
277
|
+
duration_seconds = event.duration_seconds
|
|
278
|
+
cached_tokens = event.cached_tokens
|
|
279
|
+
cache_creation_tokens = event.cache_creation_input_tokens
|
|
280
|
+
cache_read_tokens = event.cache_read_input_tokens
|
|
281
|
+
|
|
282
|
+
summary_text = self._build_cost_summary_text(
|
|
283
|
+
cost, tokens, None, include_emojis=False, duration_seconds=duration_seconds
|
|
284
|
+
)
|
|
285
|
+
if not summary_text:
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
# Add cache statistics if available
|
|
289
|
+
cache_parts = []
|
|
290
|
+
if cached_tokens and cached_tokens > 0:
|
|
291
|
+
cache_parts.append(f"Cached: {cached_tokens:,} tokens")
|
|
292
|
+
if cache_creation_tokens and cache_creation_tokens > 0:
|
|
293
|
+
cache_parts.append(f"Cache write: {cache_creation_tokens:,} tokens")
|
|
294
|
+
if cache_read_tokens and cache_read_tokens > 0:
|
|
295
|
+
cache_parts.append(f"Cache read: {cache_read_tokens:,} tokens")
|
|
296
|
+
|
|
297
|
+
if cache_parts:
|
|
298
|
+
cache_summary = " | ".join(cache_parts)
|
|
299
|
+
self.console.print(f"\n{summary_text}")
|
|
300
|
+
self.console.print(f"{cache_summary}\n")
|
|
301
|
+
else:
|
|
302
|
+
self.console.print(f"\n{summary_text}\n")
|
|
303
|
+
|
|
304
|
+
def _handle_execution_result(self, event: ExecutionResultEvent) -> None:
|
|
305
|
+
"""Handle code execution result event with plain text output."""
|
|
306
|
+
if not self.show_execution_results:
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
self.console.print("Processing execution results...")
|
|
310
|
+
|
|
311
|
+
# Display execution logs if present
|
|
312
|
+
if event.logs:
|
|
313
|
+
logs_text = "\n".join(event.logs)
|
|
314
|
+
if logs_text.strip():
|
|
315
|
+
self.console.print(f"Logs: {logs_text}")
|
|
316
|
+
|
|
317
|
+
# Display output if present and meaningful
|
|
318
|
+
if event.output:
|
|
319
|
+
output_text = event.output
|
|
320
|
+
if output_text.strip():
|
|
321
|
+
contains_error = self._contains_error(output_text)
|
|
322
|
+
is_final_answer = self._is_final_answer(output_text)
|
|
323
|
+
|
|
324
|
+
# Always show errors, filter non-meaningful outputs otherwise
|
|
325
|
+
if contains_error:
|
|
326
|
+
self.console.print()
|
|
327
|
+
self.console.rule("ERROR IN OUTPUT", style="dim")
|
|
328
|
+
self.console.print(output_text)
|
|
329
|
+
self.console.rule(style="dim")
|
|
330
|
+
self.console.print()
|
|
331
|
+
elif is_final_answer:
|
|
332
|
+
# Skip displaying final answer here - it will be displayed by _handle_final_answer event
|
|
333
|
+
pass
|
|
334
|
+
elif output_text.strip() and output_text.strip().lower() not in ("none", "null", ""):
|
|
335
|
+
self.console.print(f"Output: {output_text}")
|
|
336
|
+
|
|
337
|
+
def _handle_execution_logs(self, event: ExecutionLogsEvent) -> None:
|
|
338
|
+
"""Handle execution logs event with plain text output."""
|
|
339
|
+
if not self.show_execution_logs:
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
content = event.logs
|
|
343
|
+
|
|
344
|
+
if content.strip() and "Execution logs:" in content:
|
|
345
|
+
# Extract just the log content
|
|
346
|
+
logs = content.replace("Execution logs:", "").strip()
|
|
347
|
+
if logs:
|
|
348
|
+
self.console.print(f"Logs: {logs}")
|
|
349
|
+
|
|
350
|
+
def _handle_subagent_start(self, event: Any) -> None:
|
|
351
|
+
"""Handle subagent start event with plain text output."""
|
|
352
|
+
agent_name = getattr(event, "agent_name", "unknown")
|
|
353
|
+
|
|
354
|
+
self.console.print(f"Spawning {agent_name}...")
|
|
355
|
+
self.console.print()
|
|
356
|
+
self.console.rule(f"{agent_name} agent")
|
|
357
|
+
self.console.print()
|
|
358
|
+
|
|
359
|
+
def _handle_subagent_end(self, event: Any) -> None:
|
|
360
|
+
"""Handle subagent end event with plain text output."""
|
|
361
|
+
agent_name = getattr(event, "agent_name", "unknown")
|
|
362
|
+
|
|
363
|
+
self.console.print()
|
|
364
|
+
self.console.rule()
|
|
365
|
+
self.console.print(f"{agent_name} completed")
|
|
366
|
+
self.console.print()
|
|
367
|
+
|
|
368
|
+
@contextmanager
|
|
369
|
+
def progress_context(self) -> Generator[None, None, None]:
|
|
370
|
+
"""Context manager for showing progress during execution.
|
|
371
|
+
|
|
372
|
+
Plain UI handler shows minimal progress spinners for subagent tracking.
|
|
373
|
+
When no_color is enabled, completely disables progress spinners to avoid ANSI codes.
|
|
374
|
+
"""
|
|
375
|
+
# If no_color is enabled, skip all progress/spinner output
|
|
376
|
+
if self.console.no_color:
|
|
377
|
+
# Just set the UI context without any progress
|
|
378
|
+
set_ui_context(console=self.console, progress=None, ui_handler=self)
|
|
379
|
+
try:
|
|
380
|
+
yield
|
|
381
|
+
finally:
|
|
382
|
+
clear_ui_context()
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
386
|
+
|
|
387
|
+
# Create progress with simple spinner for minimal UI
|
|
388
|
+
# Non-transient so subagent spinners remain visible during execution
|
|
389
|
+
progress = Progress(
|
|
390
|
+
SpinnerColumn(),
|
|
391
|
+
TextColumn("[progress.description]{task.description}"),
|
|
392
|
+
console=self.console,
|
|
393
|
+
transient=False,
|
|
394
|
+
refresh_per_second=4, # Ensure regular refreshes for spinner animation
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Store console, progress, and ui_handler in thread-local for tool access
|
|
398
|
+
set_ui_context(console=self.console, progress=progress, ui_handler=self)
|
|
399
|
+
|
|
400
|
+
# Use context manager to start live rendering
|
|
401
|
+
with progress:
|
|
402
|
+
# Add a hidden task to keep Progress Live display active
|
|
403
|
+
# Without this, the Live display may not render properly when tasks are added dynamically
|
|
404
|
+
dummy_task = progress.add_task("[dim]Executing...[/dim]", total=None)
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
yield
|
|
408
|
+
finally:
|
|
409
|
+
if dummy_task is not None:
|
|
410
|
+
progress.remove_task(dummy_task)
|
|
411
|
+
clear_ui_context()
|
|
412
|
+
|
|
413
|
+
def update_progress(self, description: str) -> None:
|
|
414
|
+
"""Update progress description.
|
|
415
|
+
|
|
416
|
+
Plain UI handler silently ignores progress updates to avoid clutter.
|
|
417
|
+
"""
|
|
418
|
+
# No-op in plain mode - we don't show progress spinners
|
|
419
|
+
pass
|