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
|
@@ -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,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()
|