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
@@ -0,0 +1,225 @@
1
+ """Textual UI handler for chat interface."""
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+
5
+ from tsugite.events import (
6
+ BaseEvent,
7
+ CodeExecutionEvent,
8
+ ErrorEvent,
9
+ ExecutionResultEvent,
10
+ FinalAnswerEvent,
11
+ LLMMessageEvent,
12
+ ObservationEvent,
13
+ StepStartEvent,
14
+ StreamChunkEvent,
15
+ StreamCompleteEvent,
16
+ TaskStartEvent,
17
+ ToolCallEvent,
18
+ )
19
+ from tsugite.ui.base import CustomUIHandler
20
+
21
+
22
+ class TextualUIHandler(CustomUIHandler):
23
+ """UI handler that updates Textual reactive variables via callbacks."""
24
+
25
+ def __init__(
26
+ self,
27
+ on_status_change: Optional[Callable[[str], None]] = None,
28
+ on_tool_call: Optional[Callable[[str], None]] = None,
29
+ on_stream_chunk: Optional[Callable[[str], None]] = None,
30
+ on_stream_complete: Optional[Callable[[], None]] = None,
31
+ on_intermediate_message: Optional[Callable[[str], None]] = None,
32
+ on_thought_log: Optional[Callable[[str, str], None]] = None,
33
+ ):
34
+ """Initialize Textual UI handler.
35
+
36
+ Args:
37
+ on_status_change: Callback for status updates
38
+ on_tool_call: Callback when tool is called
39
+ on_stream_chunk: Callback for streaming chunks
40
+ on_stream_complete: Callback when streaming completes
41
+ on_intermediate_message: Callback for intermediate agent messages
42
+ on_thought_log: Callback for thought log entries (type, content)
43
+ """
44
+ # Initialize with a no-op console (we won't use it)
45
+ from io import StringIO
46
+
47
+ from rich.console import Console
48
+
49
+ # Use a StringIO console to capture any output we don't want
50
+ super().__init__(
51
+ console=Console(file=StringIO(), force_terminal=False),
52
+ show_panels=False, # Don't render panels
53
+ )
54
+
55
+ self.on_status_change = on_status_change
56
+ self.on_tool_call = on_tool_call
57
+ self.on_stream_chunk = on_stream_chunk
58
+ self.on_stream_complete = on_stream_complete
59
+ self.on_intermediate_message = on_intermediate_message
60
+ self.on_thought_log = on_thought_log
61
+
62
+ # Track tools used in current turn
63
+ self.current_tools = []
64
+ self._last_status = ""
65
+
66
+ def handle_event(self, event: BaseEvent) -> None:
67
+ """Handle UI event by calling appropriate callbacks."""
68
+ with self._lock:
69
+ if isinstance(event, TaskStartEvent):
70
+ self._handle_task_start(event)
71
+ elif isinstance(event, StepStartEvent):
72
+ self._handle_step_start(event)
73
+ elif isinstance(event, CodeExecutionEvent):
74
+ self._handle_code_execution(event)
75
+ elif isinstance(event, ToolCallEvent):
76
+ self._handle_tool_call(event)
77
+ elif isinstance(event, ObservationEvent):
78
+ self._handle_observation(event)
79
+ elif isinstance(event, FinalAnswerEvent):
80
+ self._handle_final_answer(event)
81
+ elif isinstance(event, ErrorEvent):
82
+ self._handle_error(event)
83
+ elif isinstance(event, LLMMessageEvent):
84
+ self._handle_llm_message(event)
85
+ elif isinstance(event, StreamChunkEvent):
86
+ self._handle_stream_chunk(event)
87
+ elif isinstance(event, StreamCompleteEvent):
88
+ self._handle_stream_complete(event)
89
+ elif isinstance(event, ExecutionResultEvent):
90
+ self._handle_execution_result(event)
91
+
92
+ def _update_status(self, new_status: str) -> None:
93
+ """Update status with thread safety.
94
+
95
+ Args:
96
+ new_status: New status message
97
+ """
98
+ if new_status != self._last_status:
99
+ self._last_status = new_status
100
+ if self.on_status_change:
101
+ self.on_status_change(new_status)
102
+
103
+ def _handle_task_start(self, event: TaskStartEvent) -> None:
104
+ """Handle task start - reset tools list."""
105
+ self.current_tools = []
106
+ self._update_status("Starting task...")
107
+
108
+ def _handle_step_start(self, event: StepStartEvent) -> None:
109
+ """Handle step start."""
110
+ step = event.step
111
+ status_msg = f"Step {step}: Waiting for LLM response..."
112
+ self._update_status(status_msg)
113
+
114
+ # Log to thought log
115
+ if self.on_thought_log:
116
+ self.on_thought_log("step", f"Step {step}")
117
+
118
+ def _handle_code_execution(self, event: CodeExecutionEvent) -> None:
119
+ """Handle code execution."""
120
+ code = event.code
121
+ # Show preview of code being executed
122
+ preview = code[:50] if code else "code"
123
+ if len(code) > 50:
124
+ preview += "..."
125
+ self._update_status(f"Executing: {preview}")
126
+
127
+ # Log to thought log instead of chat
128
+ if self.on_thought_log:
129
+ self.on_thought_log("code_execution", code)
130
+
131
+ def _handle_tool_call(self, event: ToolCallEvent) -> None:
132
+ """Handle tool call."""
133
+ tool_name = event.tool
134
+ self.current_tools.append(tool_name)
135
+
136
+ self._update_status(f"Using tool: {tool_name}")
137
+ if self.on_tool_call:
138
+ self.on_tool_call(tool_name)
139
+
140
+ # Log to thought log instead of chat
141
+ if self.on_thought_log:
142
+ self.on_thought_log("tool_call", tool_name)
143
+
144
+ def _handle_observation(self, event: ObservationEvent) -> None:
145
+ """Handle observation."""
146
+ observation = event.observation
147
+ self._update_status("Processing results...")
148
+
149
+ # Log to thought log if meaningful
150
+ if self.on_thought_log and observation.strip():
151
+ # Clean up observation and truncate if too long
152
+ clean_obs = observation.replace("|", "[").strip()
153
+ if len(clean_obs) > 150:
154
+ clean_obs = clean_obs[:150] + "..."
155
+ self.on_thought_log("observation", clean_obs)
156
+
157
+ def _handle_final_answer(self, event: FinalAnswerEvent) -> None:
158
+ """Handle final answer."""
159
+ self._update_status("Finalizing answer...")
160
+
161
+ def _handle_error(self, event: ErrorEvent) -> None:
162
+ """Handle error."""
163
+ error = event.error
164
+ self._update_status(f"Error: {error}")
165
+
166
+ def _handle_llm_message(self, event: LLMMessageEvent) -> None:
167
+ """Handle intermediate LLM message (thoughts from agent)."""
168
+ content = event.content
169
+ if content:
170
+ # Route thoughts to thought log instead of chat
171
+ if self.on_thought_log:
172
+ # Clean up the content - remove any leading/trailing whitespace
173
+ clean_content = content.strip()
174
+ if clean_content:
175
+ self.on_thought_log("step", f"💭 {clean_content}")
176
+
177
+ def _handle_stream_chunk(self, event: StreamChunkEvent) -> None:
178
+ """Handle streaming chunk."""
179
+ chunk = event.chunk
180
+ self.streaming_content += chunk
181
+ self.is_streaming = True
182
+
183
+ self._update_status("Streaming response...")
184
+ if self.on_stream_chunk:
185
+ self.on_stream_chunk(chunk)
186
+
187
+ def _handle_stream_complete(self, data: Dict[str, Any]) -> None:
188
+ """Handle streaming complete."""
189
+ self.is_streaming = False
190
+ self.streaming_content = ""
191
+
192
+ self._update_status("Ready")
193
+ if self.on_stream_complete:
194
+ self.on_stream_complete()
195
+
196
+ def get_tools_used(self):
197
+ """Get list of tools used in current turn."""
198
+ return self.current_tools.copy()
199
+
200
+ def clear_tools(self):
201
+ """Clear tools list for new turn."""
202
+ self.current_tools = []
203
+
204
+ def _handle_execution_result(self, event: ExecutionResultEvent) -> None:
205
+ """Handle execution result."""
206
+ content = event.result
207
+
208
+ if self.on_thought_log and content.strip():
209
+ # Parse execution result content
210
+ lines = content.split("\n")
211
+ output_lines = []
212
+
213
+ # Extract meaningful output
214
+ for line in lines:
215
+ if line.startswith("Out:"):
216
+ output_lines.append(line[4:].strip())
217
+ elif not line.startswith("Execution logs:") and line.strip():
218
+ output_lines.append(line.strip())
219
+
220
+ if output_lines:
221
+ result_text = "\n".join(output_lines)
222
+ # Truncate if too long
223
+ if len(result_text) > 200:
224
+ result_text = result_text[:200] + "..."
225
+ self.on_thought_log("execution_result", result_text)
@@ -0,0 +1,6 @@
1
+ """Textual widgets for Tsugite chat UI."""
2
+
3
+ from .message_list import MessageList
4
+ from .thought_log import ThoughtLog
5
+
6
+ __all__ = ["MessageList", "ThoughtLog"]
@@ -0,0 +1,27 @@
1
+ """Base class for scrollable reactive log widgets."""
2
+
3
+ from textual.containers import VerticalScroll
4
+ from textual.reactive import reactive
5
+
6
+
7
+ class BaseScrollLog(VerticalScroll):
8
+ """Base class for scrollable log widgets with reactive entry lists."""
9
+
10
+ entries = reactive([], recompose=True)
11
+
12
+ def __init__(self):
13
+ """Initialize base scroll log."""
14
+ super().__init__()
15
+ self.can_focus = True
16
+
17
+ def watch_entries(self):
18
+ """Called when entries change - auto-scroll to bottom."""
19
+ self.call_after_refresh(self.scroll_end)
20
+
21
+ def on_mount(self):
22
+ """Called when widget is mounted - initial scroll."""
23
+ self.scroll_end(animate=False)
24
+
25
+ def clear_log(self):
26
+ """Clear all entries from the log."""
27
+ self.entries = []
@@ -0,0 +1,121 @@
1
+ """Scrollable message history widget for chat UI."""
2
+
3
+ from rich.console import Group, RenderableType
4
+ from rich.markdown import Markdown
5
+ from rich.text import Text
6
+ from textual.app import ComposeResult
7
+ from textual.reactive import reactive
8
+ from textual.widgets import Static
9
+
10
+ from .base_scroll_log import BaseScrollLog
11
+
12
+
13
+ class Message(Static):
14
+ """Individual message display widget."""
15
+
16
+ def __init__(
17
+ self,
18
+ sender: str,
19
+ content: str,
20
+ sender_style: str = "bold",
21
+ content_style: str = "",
22
+ render_markdown: bool = False,
23
+ ):
24
+ """Initialize message widget.
25
+
26
+ Args:
27
+ sender: Sender label (e.g., "You", "Agent")
28
+ content: Message content
29
+ sender_style: Rich style for sender label
30
+ content_style: Rich style for content
31
+ render_markdown: Whether to render content as markdown
32
+ """
33
+ super().__init__(markup=False)
34
+ self.sender = sender
35
+ self._message_content = content
36
+ self.sender_style = sender_style
37
+ self.content_style = content_style
38
+ self.render_markdown = render_markdown
39
+
40
+ def render(self) -> RenderableType:
41
+ """Render the message with styling."""
42
+ # Create sender label
43
+ sender_text = Text()
44
+ sender_text.append(f"{self.sender}: ", style=self.sender_style)
45
+
46
+ # Render content based on mode
47
+ if self.render_markdown:
48
+ # Use markdown rendering
49
+ content_rendered = Markdown(str(self._message_content), code_theme="monokai", inline_code_theme="monokai")
50
+ # Group sender and markdown content together
51
+ return Group(sender_text, content_rendered)
52
+ else:
53
+ # Use plain text rendering
54
+ sender_text.append(str(self._message_content), style=self.content_style)
55
+ return sender_text
56
+
57
+
58
+ class MessageList(BaseScrollLog):
59
+ """Scrollable container for chat messages."""
60
+
61
+ messages = reactive([], recompose=True)
62
+ markdown_mode = reactive(True, recompose=True)
63
+
64
+ def __init__(self):
65
+ """Initialize message list."""
66
+ super().__init__()
67
+
68
+ def compose(self) -> ComposeResult:
69
+ """Compose the message list from reactive messages."""
70
+ for msg in self.messages:
71
+ if msg["type"] == "user":
72
+ yield Message("You", msg["content"], sender_style="bold #b8bb26", content_style="#b8bb26")
73
+ elif msg["type"] == "agent":
74
+ # Render agent messages with markdown if enabled
75
+ yield Message(
76
+ "Agent",
77
+ msg["content"],
78
+ sender_style="bold #83a598",
79
+ content_style="#83a598",
80
+ render_markdown=self.markdown_mode,
81
+ )
82
+ elif msg["type"] == "tool_call":
83
+ yield Message("🔧 Tool", msg["content"], sender_style="#fabd2f", content_style="dim #fabd2f")
84
+ elif msg["type"] == "code_execution":
85
+ yield Message("⚡ Code", msg["content"], sender_style="#d3869b", content_style="dim")
86
+ elif msg["type"] == "execution_result":
87
+ yield Message("📤 Result", msg["content"], sender_style="#8ec07c", content_style="dim #8ec07c")
88
+ elif msg["type"] == "observation":
89
+ yield Message("💡 Info", msg["content"], sender_style="#83a598", content_style="dim #83a598")
90
+ elif msg["type"] == "separator":
91
+ yield Static("─" * 40, classes="separator", markup=False)
92
+ elif msg["type"] == "status":
93
+ yield Static(msg["content"], classes="status", markup=False)
94
+
95
+ def add_message(self, sender_type: str, content: str):
96
+ """Add a new message to the list.
97
+
98
+ Args:
99
+ sender_type: Type of sender ("user", "agent", "status", "separator")
100
+ content: Message content
101
+ """
102
+ new_messages = self.messages.copy()
103
+ new_messages.append({"type": sender_type, "content": content})
104
+ self.messages = new_messages
105
+
106
+ def add_separator(self):
107
+ """Add a visual separator between exchanges."""
108
+ self.add_message("separator", "")
109
+
110
+ def toggle_markdown(self) -> bool:
111
+ """Toggle markdown rendering mode.
112
+
113
+ Returns:
114
+ New markdown mode state
115
+ """
116
+ self.markdown_mode = not self.markdown_mode
117
+ return self.markdown_mode
118
+
119
+ def watch_messages(self):
120
+ """Called when messages change - auto-scroll to bottom."""
121
+ self.call_after_refresh(self.scroll_end)
@@ -0,0 +1,80 @@
1
+ """Thought log widget showing execution summaries and LLM thinking."""
2
+
3
+ from rich.text import Text
4
+ from textual.app import ComposeResult
5
+ from textual.widgets import Static
6
+
7
+ from .base_scroll_log import BaseScrollLog
8
+
9
+
10
+ class ThoughtEntry(Static):
11
+ """Individual thought/execution entry."""
12
+
13
+ def __init__(self, icon: str, content: str, style: str = "dim"):
14
+ """Initialize thought entry.
15
+
16
+ Args:
17
+ icon: Emoji or symbol for entry type
18
+ content: Brief description of what's happening
19
+ style: Rich style for content
20
+ """
21
+ # Store values before calling super().__init__
22
+ self.icon = icon
23
+ self.entry_content = content # Use different name to avoid conflict with Static.content
24
+ self.entry_style = style
25
+ super().__init__()
26
+
27
+ def render(self) -> Text:
28
+ """Render the thought entry."""
29
+ text = Text()
30
+ text.append(f"{self.icon} ", style=self.entry_style)
31
+ text.append(str(self.entry_content), style=self.entry_style)
32
+ return text
33
+
34
+
35
+ class ThoughtLog(BaseScrollLog):
36
+ """Scrollable log of agent thoughts and execution summaries."""
37
+
38
+ def __init__(self):
39
+ """Initialize thought log."""
40
+ super().__init__()
41
+
42
+ def compose(self) -> ComposeResult:
43
+ """Compose the thought log from reactive entries."""
44
+ for entry in self.entries:
45
+ entry_type = entry["type"]
46
+ content = entry["content"]
47
+
48
+ if entry_type == "step":
49
+ yield ThoughtEntry("🧠", content, style="bold #83a598") # gruvbox blue
50
+ elif entry_type == "tool_call":
51
+ yield ThoughtEntry("🔧", f"Using tool: {content}", style="#fabd2f") # gruvbox yellow
52
+ elif entry_type == "code_execution":
53
+ # Summarize code execution
54
+ code = content
55
+ lines = code.count("\n") + 1
56
+ preview = code[:40].replace("\n", " ") if code else "code"
57
+ if len(code) > 40:
58
+ preview += "..."
59
+ yield ThoughtEntry(
60
+ "⚡", f"Executing {lines} line(s) of code: {preview}", style="#d3869b"
61
+ ) # gruvbox purple
62
+ elif entry_type == "observation":
63
+ yield ThoughtEntry("💡", f"Result: {content}", style="#8ec07c") # gruvbox aqua
64
+ elif entry_type == "execution_result":
65
+ yield ThoughtEntry("📤", f"Output: {content}", style="#8ec07c") # gruvbox aqua
66
+ elif entry_type == "status":
67
+ yield ThoughtEntry("ℹ️", content, style="dim #a89984") # gruvbox gray
68
+ elif entry_type == "error":
69
+ yield ThoughtEntry("❌", content, style="bold #fb4934") # gruvbox red
70
+
71
+ def add_entry(self, entry_type: str, content: str):
72
+ """Add a new entry to the thought log.
73
+
74
+ Args:
75
+ entry_type: Type of entry (step, tool_call, code_execution, etc.)
76
+ content: Entry content/description
77
+ """
78
+ new_entries = self.entries.copy()
79
+ new_entries.append({"type": entry_type, "content": content})
80
+ self.entries = new_entries
tsugite/ui_context.py ADDED
@@ -0,0 +1,90 @@
1
+ """Context for UI state access from tools using contextvars."""
2
+
3
+ import contextvars
4
+ from contextlib import contextmanager
5
+ from typing import Any, Generator, Optional
6
+
7
+ from rich.console import Console
8
+ from rich.progress import Progress
9
+
10
+ # Context variables for UI context (can be propagated to threads)
11
+ _console_var: contextvars.ContextVar[Optional[Console]] = contextvars.ContextVar("console", default=None)
12
+ _progress_var: contextvars.ContextVar[Optional[Progress]] = contextvars.ContextVar("progress", default=None)
13
+ _ui_handler_var: contextvars.ContextVar[Optional[Any]] = contextvars.ContextVar("ui_handler", default=None)
14
+
15
+
16
+ def set_ui_context(
17
+ console: Optional[Console] = None,
18
+ progress: Optional[Progress] = None,
19
+ ui_handler: Optional[Any] = None,
20
+ ) -> None:
21
+ """Store UI context in context variables.
22
+
23
+ Args:
24
+ console: Rich console instance
25
+ progress: Rich progress instance (spinner)
26
+ ui_handler: UI handler instance (e.g., TextualUIHandler or CustomUIHandler)
27
+ """
28
+ _console_var.set(console)
29
+ _progress_var.set(progress)
30
+ _ui_handler_var.set(ui_handler)
31
+
32
+
33
+ def get_console() -> Optional[Console]:
34
+ """Get the console from context variables."""
35
+ return _console_var.get()
36
+
37
+
38
+ def get_progress() -> Optional[Progress]:
39
+ """Get the progress spinner from context variables."""
40
+ return _progress_var.get()
41
+
42
+
43
+ def get_ui_handler() -> Optional[Any]:
44
+ """Get the UI handler from context variables.
45
+
46
+ Returns:
47
+ UI handler instance if available (e.g., TextualUIHandler), None otherwise
48
+ """
49
+ return _ui_handler_var.get()
50
+
51
+
52
+ def clear_ui_context() -> None:
53
+ """Clear UI context from context variables."""
54
+ _console_var.set(None)
55
+ _progress_var.set(None)
56
+ _ui_handler_var.set(None)
57
+
58
+
59
+ @contextmanager
60
+ def paused_progress() -> Generator[None, None, None]:
61
+ """Context manager to temporarily pause UI elements for user input.
62
+
63
+ This pauses either:
64
+ - Live Display (if a UI handler with pause_for_input exists)
65
+ - Progress spinner (fallback for older UI modes)
66
+
67
+ This is useful for interactive operations that need user input
68
+ without UI elements interfering with the display.
69
+
70
+ Yields:
71
+ None
72
+ """
73
+ ui_handler = get_ui_handler()
74
+ progress = get_progress()
75
+
76
+ # Check if UI handler has a pause_for_input method (Live Display)
77
+ if ui_handler is not None and hasattr(ui_handler, "pause_for_input"):
78
+ # Use the UI handler's pause mechanism (for Live Display)
79
+ with ui_handler.pause_for_input():
80
+ yield
81
+ else:
82
+ # Fallback: pause old-style progress spinner
83
+ if progress is not None:
84
+ progress.stop()
85
+
86
+ try:
87
+ yield
88
+ finally:
89
+ if progress is not None:
90
+ progress.start()