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.
Files changed (117) hide show
  1. vtx/__init__.py +63 -0
  2. vtx/async_utils.py +40 -0
  3. vtx/builtin_skills/github/SKILL.md +139 -0
  4. vtx/builtin_skills/init/SKILL.md +74 -0
  5. vtx/builtin_skills/review/SKILL.md +73 -0
  6. vtx/builtin_skills/skill-builder/SKILL.md +133 -0
  7. vtx/cli.py +90 -0
  8. vtx/config.py +741 -0
  9. vtx/context/__init__.py +15 -0
  10. vtx/context/_xml.py +8 -0
  11. vtx/context/agent_mds.py +128 -0
  12. vtx/context/git.py +64 -0
  13. vtx/context/loader.py +41 -0
  14. vtx/context/skills.py +423 -0
  15. vtx/core/__init__.py +47 -0
  16. vtx/core/compaction.py +89 -0
  17. vtx/core/errors.py +17 -0
  18. vtx/core/handoff.py +51 -0
  19. vtx/core/scratchpad.py +54 -0
  20. vtx/core/types.py +197 -0
  21. vtx/defaults/__init__.py +0 -0
  22. vtx/defaults/config.yml +53 -0
  23. vtx/diff_display.py +12 -0
  24. vtx/events.py +224 -0
  25. vtx/gh_cli.py +82 -0
  26. vtx/git_branch.py +90 -0
  27. vtx/headless.py +127 -0
  28. vtx/llm/__init__.py +93 -0
  29. vtx/llm/base.py +217 -0
  30. vtx/llm/context_length.py +150 -0
  31. vtx/llm/dynamic_models.py +735 -0
  32. vtx/llm/model_fetcher.py +279 -0
  33. vtx/llm/models.py +78 -0
  34. vtx/llm/oauth/__init__.py +59 -0
  35. vtx/llm/oauth/copilot.py +358 -0
  36. vtx/llm/oauth/dynamic.py +236 -0
  37. vtx/llm/oauth/openai.py +400 -0
  38. vtx/llm/phase_parser.py +270 -0
  39. vtx/llm/provider.yaml +280 -0
  40. vtx/llm/provider_catalog.py +230 -0
  41. vtx/llm/providers/__init__.py +45 -0
  42. vtx/llm/providers/anthropic_sdk.py +256 -0
  43. vtx/llm/providers/mock.py +249 -0
  44. vtx/llm/providers/openai_sdk.py +246 -0
  45. vtx/llm/providers/sanitize.py +14 -0
  46. vtx/llm/sdk/__init__.py +13 -0
  47. vtx/llm/sdk/anthropic.py +382 -0
  48. vtx/llm/sdk/base.py +82 -0
  49. vtx/llm/sdk/openai.py +344 -0
  50. vtx/llm/tool_parser.py +161 -0
  51. vtx/loop.py +272 -0
  52. vtx/notify.py +109 -0
  53. vtx/permissions.py +114 -0
  54. vtx/prompts/__init__.py +45 -0
  55. vtx/prompts/builder.py +86 -0
  56. vtx/prompts/env.py +58 -0
  57. vtx/prompts/identity.py +166 -0
  58. vtx/prompts/tooling.py +36 -0
  59. vtx/py.typed +0 -0
  60. vtx/runtime.py +580 -0
  61. vtx/session.py +868 -0
  62. vtx/sounds/completion.wav +0 -0
  63. vtx/sounds/error.wav +0 -0
  64. vtx/sounds/permission.wav +0 -0
  65. vtx/themes.py +1104 -0
  66. vtx/tools/__init__.py +68 -0
  67. vtx/tools/_read_image.py +106 -0
  68. vtx/tools/_tool_utils.py +90 -0
  69. vtx/tools/base.py +36 -0
  70. vtx/tools/bash.py +371 -0
  71. vtx/tools/edit.py +261 -0
  72. vtx/tools/find.py +132 -0
  73. vtx/tools/read.py +238 -0
  74. vtx/tools/skill.py +278 -0
  75. vtx/tools/web.py +238 -0
  76. vtx/tools/write.py +88 -0
  77. vtx/tools_manager.py +216 -0
  78. vtx/turn.py +789 -0
  79. vtx/ui/__init__.py +0 -0
  80. vtx/ui/agent_runner.py +417 -0
  81. vtx/ui/app.py +665 -0
  82. vtx/ui/app_protocol.py +29 -0
  83. vtx/ui/autocomplete.py +440 -0
  84. vtx/ui/blocks.py +735 -0
  85. vtx/ui/chat.py +613 -0
  86. vtx/ui/clipboard.py +59 -0
  87. vtx/ui/commands/__init__.py +100 -0
  88. vtx/ui/commands/auth.py +306 -0
  89. vtx/ui/commands/base.py +122 -0
  90. vtx/ui/commands/models.py +144 -0
  91. vtx/ui/commands/sessions.py +388 -0
  92. vtx/ui/commands/settings.py +286 -0
  93. vtx/ui/completion_ui.py +313 -0
  94. vtx/ui/export.py +703 -0
  95. vtx/ui/floating_list.py +370 -0
  96. vtx/ui/formatting.py +287 -0
  97. vtx/ui/input.py +760 -0
  98. vtx/ui/latex.py +349 -0
  99. vtx/ui/launch.py +108 -0
  100. vtx/ui/path_complete.py +228 -0
  101. vtx/ui/prompt_history.py +102 -0
  102. vtx/ui/queue_ui.py +141 -0
  103. vtx/ui/selection_mode.py +18 -0
  104. vtx/ui/session_ui.py +235 -0
  105. vtx/ui/startup.py +124 -0
  106. vtx/ui/styles.py +327 -0
  107. vtx/ui/tool_output.py +34 -0
  108. vtx/ui/tree.py +437 -0
  109. vtx/ui/welcome.py +51 -0
  110. vtx/ui/widgets.py +558 -0
  111. vtx/update_check.py +49 -0
  112. vtx/version.py +22 -0
  113. vtx_coding_agent-0.1.1.dist-info/METADATA +259 -0
  114. vtx_coding_agent-0.1.1.dist-info/RECORD +117 -0
  115. vtx_coding_agent-0.1.1.dist-info/WHEEL +4 -0
  116. vtx_coding_agent-0.1.1.dist-info/entry_points.txt +2 -0
  117. 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")