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.
Files changed (101) hide show
  1. tsugite/__init__.py +6 -0
  2. tsugite/agent_composition.py +163 -0
  3. tsugite/agent_inheritance.py +479 -0
  4. tsugite/agent_preparation.py +236 -0
  5. tsugite/agent_runner/__init__.py +45 -0
  6. tsugite/agent_runner/helpers.py +106 -0
  7. tsugite/agent_runner/history_integration.py +248 -0
  8. tsugite/agent_runner/metrics.py +100 -0
  9. tsugite/agent_runner/runner.py +1879 -0
  10. tsugite/agent_runner/validation.py +70 -0
  11. tsugite/agent_utils.py +167 -0
  12. tsugite/attachments/__init__.py +65 -0
  13. tsugite/attachments/auto_context.py +199 -0
  14. tsugite/attachments/base.py +34 -0
  15. tsugite/attachments/file.py +51 -0
  16. tsugite/attachments/inline.py +31 -0
  17. tsugite/attachments/storage.py +178 -0
  18. tsugite/attachments/url.py +59 -0
  19. tsugite/attachments/youtube.py +101 -0
  20. tsugite/benchmark/__init__.py +62 -0
  21. tsugite/benchmark/config.py +183 -0
  22. tsugite/benchmark/core.py +292 -0
  23. tsugite/benchmark/discovery.py +377 -0
  24. tsugite/benchmark/evaluators.py +671 -0
  25. tsugite/benchmark/execution.py +657 -0
  26. tsugite/benchmark/metrics.py +204 -0
  27. tsugite/benchmark/reports.py +420 -0
  28. tsugite/benchmark/utils.py +288 -0
  29. tsugite/builtin_agents/chat-assistant.md +53 -0
  30. tsugite/builtin_agents/default.md +140 -0
  31. tsugite/builtin_agents.py +5 -0
  32. tsugite/cache.py +195 -0
  33. tsugite/cli/__init__.py +1042 -0
  34. tsugite/cli/agents.py +148 -0
  35. tsugite/cli/attachments.py +193 -0
  36. tsugite/cli/benchmark.py +663 -0
  37. tsugite/cli/cache.py +113 -0
  38. tsugite/cli/config.py +272 -0
  39. tsugite/cli/helpers.py +534 -0
  40. tsugite/cli/history.py +193 -0
  41. tsugite/cli/init.py +387 -0
  42. tsugite/cli/mcp.py +193 -0
  43. tsugite/cli/tools.py +419 -0
  44. tsugite/config.py +204 -0
  45. tsugite/console.py +48 -0
  46. tsugite/constants.py +21 -0
  47. tsugite/core/__init__.py +19 -0
  48. tsugite/core/agent.py +774 -0
  49. tsugite/core/executor.py +300 -0
  50. tsugite/core/memory.py +67 -0
  51. tsugite/core/tools.py +271 -0
  52. tsugite/docker_cli.py +270 -0
  53. tsugite/events/__init__.py +55 -0
  54. tsugite/events/base.py +46 -0
  55. tsugite/events/bus.py +62 -0
  56. tsugite/events/events.py +224 -0
  57. tsugite/exceptions.py +40 -0
  58. tsugite/history/__init__.py +29 -0
  59. tsugite/history/index.py +210 -0
  60. tsugite/history/models.py +106 -0
  61. tsugite/history/storage.py +157 -0
  62. tsugite/mcp_client.py +219 -0
  63. tsugite/mcp_config.py +174 -0
  64. tsugite/md_agents.py +751 -0
  65. tsugite/models.py +257 -0
  66. tsugite/renderer.py +151 -0
  67. tsugite/shell_tool_config.py +265 -0
  68. tsugite/templates/assistant.md +14 -0
  69. tsugite/tools/__init__.py +265 -0
  70. tsugite/tools/agents.py +312 -0
  71. tsugite/tools/edit_strategies.py +393 -0
  72. tsugite/tools/fs.py +329 -0
  73. tsugite/tools/http.py +239 -0
  74. tsugite/tools/interactive.py +430 -0
  75. tsugite/tools/shell.py +129 -0
  76. tsugite/tools/shell_tools.py +214 -0
  77. tsugite/tools/tasks.py +339 -0
  78. tsugite/tsugite.py +7 -0
  79. tsugite/ui/__init__.py +46 -0
  80. tsugite/ui/base.py +638 -0
  81. tsugite/ui/chat.py +265 -0
  82. tsugite/ui/chat.tcss +92 -0
  83. tsugite/ui/chat_history.py +286 -0
  84. tsugite/ui/helpers.py +102 -0
  85. tsugite/ui/jsonl.py +125 -0
  86. tsugite/ui/live_template.py +529 -0
  87. tsugite/ui/plain.py +419 -0
  88. tsugite/ui/textual_chat.py +642 -0
  89. tsugite/ui/textual_handler.py +225 -0
  90. tsugite/ui/widgets/__init__.py +6 -0
  91. tsugite/ui/widgets/base_scroll_log.py +27 -0
  92. tsugite/ui/widgets/message_list.py +121 -0
  93. tsugite/ui/widgets/thought_log.py +80 -0
  94. tsugite/ui_context.py +90 -0
  95. tsugite/utils.py +367 -0
  96. tsugite/xdg.py +104 -0
  97. tsugite_cli-0.3.3.dist-info/METADATA +325 -0
  98. tsugite_cli-0.3.3.dist-info/RECORD +101 -0
  99. tsugite_cli-0.3.3.dist-info/WHEEL +4 -0
  100. tsugite_cli-0.3.3.dist-info/entry_points.txt +5 -0
  101. 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