vtx-coding-agent 0.1.1__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.
- vtx/__init__.py +63 -0
- vtx/async_utils.py +40 -0
- vtx/builtin_skills/github/SKILL.md +139 -0
- vtx/builtin_skills/init/SKILL.md +74 -0
- vtx/builtin_skills/review/SKILL.md +73 -0
- vtx/builtin_skills/skill-builder/SKILL.md +133 -0
- vtx/cli.py +90 -0
- vtx/config.py +741 -0
- vtx/context/__init__.py +15 -0
- vtx/context/_xml.py +8 -0
- vtx/context/agent_mds.py +128 -0
- vtx/context/git.py +64 -0
- vtx/context/loader.py +41 -0
- vtx/context/skills.py +423 -0
- vtx/core/__init__.py +47 -0
- vtx/core/compaction.py +89 -0
- vtx/core/errors.py +17 -0
- vtx/core/handoff.py +51 -0
- vtx/core/scratchpad.py +54 -0
- vtx/core/types.py +197 -0
- vtx/defaults/__init__.py +0 -0
- vtx/defaults/config.yml +53 -0
- vtx/diff_display.py +12 -0
- vtx/events.py +224 -0
- vtx/gh_cli.py +82 -0
- vtx/git_branch.py +90 -0
- vtx/headless.py +127 -0
- vtx/llm/__init__.py +93 -0
- vtx/llm/base.py +217 -0
- vtx/llm/context_length.py +150 -0
- vtx/llm/dynamic_models.py +735 -0
- vtx/llm/model_fetcher.py +279 -0
- vtx/llm/models.py +78 -0
- vtx/llm/oauth/__init__.py +59 -0
- vtx/llm/oauth/copilot.py +358 -0
- vtx/llm/oauth/dynamic.py +236 -0
- vtx/llm/oauth/openai.py +400 -0
- vtx/llm/phase_parser.py +270 -0
- vtx/llm/provider.yaml +280 -0
- vtx/llm/provider_catalog.py +230 -0
- vtx/llm/providers/__init__.py +45 -0
- vtx/llm/providers/anthropic_sdk.py +256 -0
- vtx/llm/providers/mock.py +249 -0
- vtx/llm/providers/openai_sdk.py +246 -0
- vtx/llm/providers/sanitize.py +14 -0
- vtx/llm/sdk/__init__.py +13 -0
- vtx/llm/sdk/anthropic.py +382 -0
- vtx/llm/sdk/base.py +82 -0
- vtx/llm/sdk/openai.py +344 -0
- vtx/llm/tool_parser.py +161 -0
- vtx/loop.py +272 -0
- vtx/notify.py +109 -0
- vtx/permissions.py +114 -0
- vtx/prompts/__init__.py +45 -0
- vtx/prompts/builder.py +86 -0
- vtx/prompts/env.py +58 -0
- vtx/prompts/identity.py +166 -0
- vtx/prompts/tooling.py +36 -0
- vtx/py.typed +0 -0
- vtx/runtime.py +580 -0
- vtx/session.py +868 -0
- vtx/sounds/completion.wav +0 -0
- vtx/sounds/error.wav +0 -0
- vtx/sounds/permission.wav +0 -0
- vtx/themes.py +1104 -0
- vtx/tools/__init__.py +68 -0
- vtx/tools/_read_image.py +106 -0
- vtx/tools/_tool_utils.py +90 -0
- vtx/tools/base.py +36 -0
- vtx/tools/bash.py +371 -0
- vtx/tools/edit.py +261 -0
- vtx/tools/find.py +132 -0
- vtx/tools/read.py +238 -0
- vtx/tools/skill.py +278 -0
- vtx/tools/web.py +238 -0
- vtx/tools/write.py +88 -0
- vtx/tools_manager.py +216 -0
- vtx/turn.py +789 -0
- vtx/ui/__init__.py +0 -0
- vtx/ui/agent_runner.py +417 -0
- vtx/ui/app.py +665 -0
- vtx/ui/app_protocol.py +29 -0
- vtx/ui/autocomplete.py +440 -0
- vtx/ui/blocks.py +735 -0
- vtx/ui/chat.py +613 -0
- vtx/ui/clipboard.py +59 -0
- vtx/ui/commands/__init__.py +100 -0
- vtx/ui/commands/auth.py +306 -0
- vtx/ui/commands/base.py +122 -0
- vtx/ui/commands/models.py +144 -0
- vtx/ui/commands/sessions.py +388 -0
- vtx/ui/commands/settings.py +286 -0
- vtx/ui/completion_ui.py +313 -0
- vtx/ui/export.py +703 -0
- vtx/ui/floating_list.py +370 -0
- vtx/ui/formatting.py +287 -0
- vtx/ui/input.py +760 -0
- vtx/ui/latex.py +349 -0
- vtx/ui/launch.py +108 -0
- vtx/ui/path_complete.py +228 -0
- vtx/ui/prompt_history.py +102 -0
- vtx/ui/queue_ui.py +141 -0
- vtx/ui/selection_mode.py +18 -0
- vtx/ui/session_ui.py +235 -0
- vtx/ui/startup.py +124 -0
- vtx/ui/styles.py +327 -0
- vtx/ui/tool_output.py +34 -0
- vtx/ui/tree.py +437 -0
- vtx/ui/welcome.py +51 -0
- vtx/ui/widgets.py +558 -0
- vtx/update_check.py +49 -0
- vtx/version.py +22 -0
- vtx_coding_agent-0.1.1.dist-info/METADATA +259 -0
- vtx_coding_agent-0.1.1.dist-info/RECORD +117 -0
- vtx_coding_agent-0.1.1.dist-info/WHEEL +4 -0
- vtx_coding_agent-0.1.1.dist-info/entry_points.txt +2 -0
- vtx_coding_agent-0.1.1.dist-info/licenses/LICENSE +201 -0
vtx/ui/__init__.py
ADDED
|
File without changes
|
vtx/ui/agent_runner.py
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""Drives agent runs: forwards agent events to the chat UI and handles ! / !! shell
|
|
2
|
+
commands typed at the prompt."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from collections import deque
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from vtx import config
|
|
11
|
+
|
|
12
|
+
from ..core.types import StopReason, ToolResultMessage
|
|
13
|
+
from ..events import (
|
|
14
|
+
AgentEndEvent,
|
|
15
|
+
AgentStartEvent,
|
|
16
|
+
CompactionEndEvent,
|
|
17
|
+
CompactionStartEvent,
|
|
18
|
+
ErrorEvent,
|
|
19
|
+
InterruptedEvent,
|
|
20
|
+
RetryEvent,
|
|
21
|
+
TextDeltaEvent,
|
|
22
|
+
TextEndEvent,
|
|
23
|
+
TextStartEvent,
|
|
24
|
+
ThinkingDeltaEvent,
|
|
25
|
+
ThinkingEndEvent,
|
|
26
|
+
ThinkingStartEvent,
|
|
27
|
+
ToolApprovalEvent,
|
|
28
|
+
ToolArgsTokenUpdateEvent,
|
|
29
|
+
ToolEndEvent,
|
|
30
|
+
ToolResultEvent,
|
|
31
|
+
ToolStartEvent,
|
|
32
|
+
TurnEndEvent,
|
|
33
|
+
TurnStartEvent,
|
|
34
|
+
WarningEvent,
|
|
35
|
+
)
|
|
36
|
+
from ..notify import NotificationEvent, notify
|
|
37
|
+
from ..permissions import ApprovalResponse
|
|
38
|
+
from ..runtime import ConversationRuntime
|
|
39
|
+
from ..tools import get_tool
|
|
40
|
+
from ..tools.bash import BashParams, BashTool
|
|
41
|
+
from .chat import ChatLog
|
|
42
|
+
from .widgets import InfoBar, StatusLine
|
|
43
|
+
|
|
44
|
+
_NOTIFY_EVENTS = (AgentEndEvent, ToolApprovalEvent)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AgentRunnerMixin:
|
|
48
|
+
_is_running: bool
|
|
49
|
+
_cancel_event: asyncio.Event | None
|
|
50
|
+
_steer_event: asyncio.Event | None
|
|
51
|
+
_interrupt_requested: bool
|
|
52
|
+
_abort_shown: bool
|
|
53
|
+
_current_block_type: str | None
|
|
54
|
+
_hide_thinking: bool
|
|
55
|
+
_approval_future: asyncio.Future[ApprovalResponse] | None
|
|
56
|
+
_approval_tool_id: str | None
|
|
57
|
+
_approval_selection: ApprovalResponse
|
|
58
|
+
_pending_session_switch_id: str | None
|
|
59
|
+
_shell_tool_counter: int
|
|
60
|
+
_pending_queue: deque[tuple[str, str]]
|
|
61
|
+
_steer_queue: deque[tuple[str, str]]
|
|
62
|
+
_runtime: ConversationRuntime
|
|
63
|
+
|
|
64
|
+
if TYPE_CHECKING:
|
|
65
|
+
app: Any
|
|
66
|
+
query_one: Any
|
|
67
|
+
run_worker: Any
|
|
68
|
+
|
|
69
|
+
def _update_queue_display(self) -> None: ...
|
|
70
|
+
def _clear_approval_state(self) -> None: ...
|
|
71
|
+
def _show_pending_update_notice_if_idle(self) -> None: ...
|
|
72
|
+
def _format_tool_result_text(
|
|
73
|
+
self, message: ToolResultMessage
|
|
74
|
+
) -> tuple[str, str | None]: ...
|
|
75
|
+
async def _load_session_by_id(self, session_id: str) -> None: ...
|
|
76
|
+
|
|
77
|
+
def _should_notify_for_event(self, event: object) -> bool:
|
|
78
|
+
return self._notification_event_type(event) is not None
|
|
79
|
+
|
|
80
|
+
def _notification_event_type(self, event: object) -> NotificationEvent | None:
|
|
81
|
+
if not config.notifications.enabled:
|
|
82
|
+
return None
|
|
83
|
+
if not isinstance(event, _NOTIFY_EVENTS):
|
|
84
|
+
return None
|
|
85
|
+
if isinstance(event, AgentEndEvent):
|
|
86
|
+
if event.stop_reason == StopReason.INTERRUPTED:
|
|
87
|
+
return None
|
|
88
|
+
if event.stop_reason == StopReason.ERROR:
|
|
89
|
+
return "error"
|
|
90
|
+
return "completion"
|
|
91
|
+
if isinstance(event, ToolApprovalEvent):
|
|
92
|
+
return "permission"
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
async def _run_agent(self, prompt: str) -> None:
|
|
96
|
+
chat = self.query_one("#chat-log", ChatLog)
|
|
97
|
+
status = self.query_one("#status-line", StatusLine)
|
|
98
|
+
info_bar = self.query_one("#info-bar", InfoBar)
|
|
99
|
+
|
|
100
|
+
agent = self._runtime.prepare_for_run()
|
|
101
|
+
if agent is None:
|
|
102
|
+
chat.add_info_message("Agent not initialized")
|
|
103
|
+
self._is_running = False
|
|
104
|
+
return
|
|
105
|
+
current_prompt = prompt
|
|
106
|
+
|
|
107
|
+
while True:
|
|
108
|
+
was_interrupted = False
|
|
109
|
+
|
|
110
|
+
self._cancel_event = asyncio.Event()
|
|
111
|
+
self._steer_event = asyncio.Event()
|
|
112
|
+
self._abort_shown = False
|
|
113
|
+
self._current_block_type = None
|
|
114
|
+
if self._interrupt_requested:
|
|
115
|
+
self._cancel_event.set()
|
|
116
|
+
|
|
117
|
+
status.set_status("working")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
async for event in agent.run(
|
|
121
|
+
current_prompt, cancel_event=self._cancel_event, steer_event=self._steer_event
|
|
122
|
+
):
|
|
123
|
+
notification_event = self._notification_event_type(event)
|
|
124
|
+
if notification_event:
|
|
125
|
+
notify(notification_event)
|
|
126
|
+
|
|
127
|
+
if await self._render_agent_event(event, chat, status, info_bar):
|
|
128
|
+
was_interrupted = True
|
|
129
|
+
|
|
130
|
+
except Exception as e:
|
|
131
|
+
chat.add_info_message(str(e), error=True)
|
|
132
|
+
|
|
133
|
+
if was_interrupted and not self._abort_shown:
|
|
134
|
+
chat.add_aborted_message("Interrupted by user")
|
|
135
|
+
self._abort_shown = True
|
|
136
|
+
|
|
137
|
+
self._interrupt_requested = False
|
|
138
|
+
self._cancel_event = None
|
|
139
|
+
self._steer_event = None
|
|
140
|
+
self._clear_approval_state()
|
|
141
|
+
status.set_status("idle")
|
|
142
|
+
|
|
143
|
+
if was_interrupted:
|
|
144
|
+
self._pending_queue.clear()
|
|
145
|
+
self._steer_queue.clear()
|
|
146
|
+
self._update_queue_display()
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
queued = self._dequeue_next_prompt()
|
|
150
|
+
if queued is None:
|
|
151
|
+
break
|
|
152
|
+
next_display, next_query = queued
|
|
153
|
+
chat.add_user_message(next_display)
|
|
154
|
+
current_prompt = next_query
|
|
155
|
+
|
|
156
|
+
self._is_running = False
|
|
157
|
+
|
|
158
|
+
if self._pending_session_switch_id:
|
|
159
|
+
session_id = self._pending_session_switch_id
|
|
160
|
+
self._pending_session_switch_id = None
|
|
161
|
+
self.run_worker(self._load_session_by_id(session_id), exclusive=True)
|
|
162
|
+
|
|
163
|
+
self._show_pending_update_notice_if_idle()
|
|
164
|
+
|
|
165
|
+
def _dequeue_next_prompt(self) -> tuple[str, str] | None:
|
|
166
|
+
# Steer messages take priority — drain steer queue first
|
|
167
|
+
if self._steer_queue:
|
|
168
|
+
queued = self._steer_queue.popleft()
|
|
169
|
+
elif self._pending_queue:
|
|
170
|
+
queued = self._pending_queue.popleft()
|
|
171
|
+
else:
|
|
172
|
+
return None
|
|
173
|
+
self._update_queue_display()
|
|
174
|
+
return queued
|
|
175
|
+
|
|
176
|
+
async def _render_agent_event(
|
|
177
|
+
self, event: object, chat: ChatLog, status: StatusLine, info_bar: InfoBar
|
|
178
|
+
) -> bool:
|
|
179
|
+
"""Render one agent event into the UI. Returns True if it signals interruption."""
|
|
180
|
+
was_interrupted = False
|
|
181
|
+
|
|
182
|
+
match event:
|
|
183
|
+
case AgentStartEvent():
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
case TurnStartEvent():
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
case ThinkingStartEvent():
|
|
190
|
+
if self._current_block_type != "thinking":
|
|
191
|
+
if self._current_block_type:
|
|
192
|
+
chat.end_block()
|
|
193
|
+
block = chat.start_thinking()
|
|
194
|
+
if self._hide_thinking:
|
|
195
|
+
block.add_class("-hidden")
|
|
196
|
+
self._current_block_type = "thinking"
|
|
197
|
+
|
|
198
|
+
case ThinkingDeltaEvent(delta=d):
|
|
199
|
+
await chat.append_to_current(d)
|
|
200
|
+
|
|
201
|
+
case ThinkingEndEvent():
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
case TextStartEvent():
|
|
205
|
+
if self._current_block_type != "content":
|
|
206
|
+
if self._current_block_type:
|
|
207
|
+
chat.end_block()
|
|
208
|
+
chat.start_content()
|
|
209
|
+
self._current_block_type = "content"
|
|
210
|
+
|
|
211
|
+
case TextDeltaEvent(delta=d):
|
|
212
|
+
await chat.append_to_current(d)
|
|
213
|
+
|
|
214
|
+
case TextEndEvent():
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
case ToolStartEvent(tool_call_id=id, tool_name=name):
|
|
218
|
+
if self._current_block_type:
|
|
219
|
+
chat.end_block()
|
|
220
|
+
tool = get_tool(name)
|
|
221
|
+
icon = tool.tool_icon if tool else "→"
|
|
222
|
+
chat.start_tool(name, id, "", icon=icon)
|
|
223
|
+
self._current_block_type = "tool_call"
|
|
224
|
+
status.increment_tool_calls()
|
|
225
|
+
status.set_streaming_tokens(0) # Reset token count for new tool
|
|
226
|
+
|
|
227
|
+
case ToolArgsTokenUpdateEvent(token_count=tc):
|
|
228
|
+
status.set_streaming_tokens(tc)
|
|
229
|
+
|
|
230
|
+
case ToolEndEvent(tool_call_id=id, display=display):
|
|
231
|
+
chat.update_tool_call_msg(id, display)
|
|
232
|
+
|
|
233
|
+
case ToolApprovalEvent(tool_call_id=id, tool_name=name, display=disp, future=f):
|
|
234
|
+
self.app.bell()
|
|
235
|
+
self._approval_selection = ApprovalResponse.APPROVE
|
|
236
|
+
chat.show_tool_approval(
|
|
237
|
+
id, preview=disp or None, selected=self._approval_selection
|
|
238
|
+
)
|
|
239
|
+
self._approval_future = f
|
|
240
|
+
self._approval_tool_id = id
|
|
241
|
+
|
|
242
|
+
case ToolResultEvent(tool_call_id=id, result=r, file_changes=fc):
|
|
243
|
+
self._approval_future = None
|
|
244
|
+
self._approval_tool_id = None
|
|
245
|
+
if r:
|
|
246
|
+
markup = True
|
|
247
|
+
ui_summary = r.ui_summary
|
|
248
|
+
ui_details = r.ui_details
|
|
249
|
+
ui_details_full = r.ui_details_full
|
|
250
|
+
if ui_summary is None and ui_details is None and r.content:
|
|
251
|
+
ui_details, ui_details_full = self._format_tool_result_text(r)
|
|
252
|
+
success = not r.is_error
|
|
253
|
+
chat.set_tool_result(
|
|
254
|
+
id,
|
|
255
|
+
ui_summary,
|
|
256
|
+
ui_details,
|
|
257
|
+
success,
|
|
258
|
+
markup=markup,
|
|
259
|
+
ui_details_full=ui_details_full,
|
|
260
|
+
)
|
|
261
|
+
if fc:
|
|
262
|
+
info_bar.update_file_changes(fc.path, fc.added, fc.removed)
|
|
263
|
+
|
|
264
|
+
case TurnEndEvent():
|
|
265
|
+
if event.assistant_message and event.assistant_message.usage:
|
|
266
|
+
usage = event.assistant_message.usage
|
|
267
|
+
info_bar.update_tokens(
|
|
268
|
+
usage.input_tokens,
|
|
269
|
+
usage.output_tokens,
|
|
270
|
+
usage.cache_read_tokens,
|
|
271
|
+
usage.cache_write_tokens,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
case InterruptedEvent():
|
|
275
|
+
was_interrupted = True
|
|
276
|
+
if self._current_block_type:
|
|
277
|
+
chat.end_block()
|
|
278
|
+
self._current_block_type = None
|
|
279
|
+
|
|
280
|
+
case CompactionStartEvent():
|
|
281
|
+
if self._current_block_type:
|
|
282
|
+
chat.end_block()
|
|
283
|
+
self._current_block_type = None
|
|
284
|
+
chat.show_spinner_status("Auto-compacting...")
|
|
285
|
+
|
|
286
|
+
case CompactionEndEvent(tokens_before=tb, aborted=ab, reason=why):
|
|
287
|
+
if ab:
|
|
288
|
+
msg = "Compaction failed"
|
|
289
|
+
if why:
|
|
290
|
+
msg += f": {why}"
|
|
291
|
+
chat.show_status(msg)
|
|
292
|
+
else:
|
|
293
|
+
chat.add_compaction_message(tb)
|
|
294
|
+
|
|
295
|
+
case RetryEvent(attempt=a, total_attempts=t, delay=d, error=e):
|
|
296
|
+
msg = f"Request failed (attempt {a}/{t}), retrying in {d}s; Error: {e}"
|
|
297
|
+
chat.add_info_message(msg, error=True)
|
|
298
|
+
|
|
299
|
+
case ErrorEvent(error=e):
|
|
300
|
+
chat.add_info_message(str(e), error=True)
|
|
301
|
+
|
|
302
|
+
case WarningEvent(warning=w):
|
|
303
|
+
chat.add_info_message(str(w), warning=True)
|
|
304
|
+
|
|
305
|
+
case AgentEndEvent(stop_reason=reason):
|
|
306
|
+
if reason == StopReason.INTERRUPTED:
|
|
307
|
+
was_interrupted = True
|
|
308
|
+
if self._current_block_type:
|
|
309
|
+
chat.end_block()
|
|
310
|
+
self._current_block_type = None
|
|
311
|
+
|
|
312
|
+
return was_interrupted
|
|
313
|
+
|
|
314
|
+
def _handle_shell_command(self, display_text: str, original_text: str) -> None:
|
|
315
|
+
"""Handle shell commands prefixed with ! or !!"""
|
|
316
|
+
if self._is_running:
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
chat = self.query_one("#chat-log", ChatLog)
|
|
320
|
+
|
|
321
|
+
# Determine if we should send output to LLM
|
|
322
|
+
send_to_llm = display_text.startswith("!!")
|
|
323
|
+
|
|
324
|
+
command_text = display_text[2:] if send_to_llm else display_text[1:]
|
|
325
|
+
command_text = command_text.strip()
|
|
326
|
+
|
|
327
|
+
if not command_text:
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
# Add user message showing the command
|
|
331
|
+
chat.add_user_message(display_text)
|
|
332
|
+
|
|
333
|
+
# Execute the command
|
|
334
|
+
self._is_running = True
|
|
335
|
+
self.run_worker(self._execute_shell_command(command_text, send_to_llm), exclusive=True)
|
|
336
|
+
|
|
337
|
+
async def _execute_shell_command(self, command: str, send_to_llm: bool) -> None:
|
|
338
|
+
"""Execute a shell command and display the result"""
|
|
339
|
+
chat = self.query_one("#chat-log", ChatLog)
|
|
340
|
+
status = self.query_one("#status-line", StatusLine)
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
# Create bash tool instance
|
|
344
|
+
bash_tool = BashTool()
|
|
345
|
+
|
|
346
|
+
# Create cancellation event for this command
|
|
347
|
+
cancel_event = asyncio.Event()
|
|
348
|
+
self._cancel_event = cancel_event
|
|
349
|
+
|
|
350
|
+
# Execute the command
|
|
351
|
+
status.set_status("running")
|
|
352
|
+
# Manual shell output should render like regular bash tool output:
|
|
353
|
+
# collapsed preview with ctrl+o expansion when details are available.
|
|
354
|
+
result = await bash_tool.execute(
|
|
355
|
+
BashParams(command=command), cancel_event=cancel_event, inline_output=False
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# Persist the command and its output so session resume and /export
|
|
359
|
+
# include manual shell commands, not just agent tool calls.
|
|
360
|
+
session = self._runtime.session
|
|
361
|
+
if session is not None:
|
|
362
|
+
prefix = "!!" if send_to_llm else "!"
|
|
363
|
+
session.append_custom_message(
|
|
364
|
+
"shell_command",
|
|
365
|
+
f"{prefix}{command}",
|
|
366
|
+
details={
|
|
367
|
+
"command": command,
|
|
368
|
+
"output": result.result or "",
|
|
369
|
+
"success": result.success,
|
|
370
|
+
},
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Start tool block and route the result through ChatLog so manual
|
|
374
|
+
# shell commands use the same rendering/expansion path as agent tools.
|
|
375
|
+
self._shell_tool_counter += 1
|
|
376
|
+
tool_id = f"shell-{self._shell_tool_counter}"
|
|
377
|
+
chat.start_tool("bash", tool_id, f"$ {command}", icon="$")
|
|
378
|
+
|
|
379
|
+
# Display the result
|
|
380
|
+
if result.success:
|
|
381
|
+
ui_summary = result.ui_summary
|
|
382
|
+
ui_details = result.ui_details
|
|
383
|
+
markup = True
|
|
384
|
+
if ui_summary is None and ui_details is None:
|
|
385
|
+
ui_summary = result.result or "(no output)"
|
|
386
|
+
markup = False
|
|
387
|
+
else:
|
|
388
|
+
ui_summary = result.ui_summary or "Command failed"
|
|
389
|
+
ui_details = result.ui_details or result.result
|
|
390
|
+
markup = True
|
|
391
|
+
|
|
392
|
+
chat.set_tool_result(
|
|
393
|
+
tool_id,
|
|
394
|
+
ui_summary,
|
|
395
|
+
ui_details,
|
|
396
|
+
result.success,
|
|
397
|
+
markup=markup,
|
|
398
|
+
ui_details_full=result.ui_details_full,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# If using !!, send output to LLM for follow-up unless the command was interrupted.
|
|
402
|
+
if send_to_llm and result.result and not cancel_event.is_set():
|
|
403
|
+
prompt = (
|
|
404
|
+
"Shell command output:\n\n```\n"
|
|
405
|
+
f"{result.result}\n```\n\nWhat would you like me to do with this?"
|
|
406
|
+
)
|
|
407
|
+
self._is_running = True
|
|
408
|
+
await self._run_agent(prompt)
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
except Exception as e:
|
|
412
|
+
chat.add_info_message(f"Error executing command: {e}", error=True)
|
|
413
|
+
finally:
|
|
414
|
+
self._is_running = False
|
|
415
|
+
self._interrupt_requested = False
|
|
416
|
+
self._cancel_event = None
|
|
417
|
+
status.set_status("idle")
|