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
@@ -0,0 +1,313 @@
1
+ """Completion list, selection-mode pickers and tree-selector message handling.
2
+
3
+ The @on-decorated handlers defined here must be re-bound in the Vtx class body:
4
+ Textual registers them through a metaclass that only scans the namespace of
5
+ classes created with it, and this mixin is a plain class.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from textual import on
13
+
14
+ from ..runtime import ConversationRuntime
15
+ from .autocomplete import FilePathProvider, PullRequestProvider, SlashCommandProvider
16
+ from .chat import ChatLog
17
+ from .floating_list import FloatingList, ListItem
18
+ from .input import InputBox
19
+ from .selection_mode import SelectionMode
20
+ from .tree import TreeSelector
21
+ from .widgets import InfoBar, StatusLine, format_path
22
+
23
+
24
+ class CompletionUIMixin:
25
+ _selection_mode: SelectionMode | None
26
+ _settings_active: bool
27
+ _settings_selected_value: str | None
28
+ _runtime: ConversationRuntime
29
+
30
+ if TYPE_CHECKING:
31
+ query_one: Any
32
+ batch_update: Any
33
+ call_later: Any
34
+ call_after_refresh: Any
35
+ run_worker: Any
36
+
37
+ def _reset_ctrl_d_delete_state(self) -> None: ...
38
+ def _show_settings_picker(self, selected_value: str | None = None) -> None: ...
39
+ def _handle_settings_select(self, item_value: str) -> str: ...
40
+ def _select_model(self, model) -> None: ...
41
+ def _select_theme(self, theme_id: str) -> None: ...
42
+ def _select_permission_mode(self, mode) -> None: ...
43
+ def _select_thinking_level(self, level: str) -> None: ...
44
+ def _select_thinking_lines(self, lines) -> None: ...
45
+ def _select_notifications_mode(self, mode) -> None: ...
46
+ def _select_login_provider(self, provider_id: str) -> None: ...
47
+ def _select_logout_provider(self, provider_id: str) -> None: ...
48
+ def _select_api_key_action(self, action: str) -> None: ...
49
+ def _render_session_entries(self, session) -> None: ...
50
+ async def _load_session(self, session_path) -> None: ...
51
+
52
+ def _is_chat_at_bottom(self) -> bool:
53
+ chat = self.query_one("#chat-log", ChatLog)
54
+ return abs(chat.max_scroll_y - chat.scroll_y) < 1
55
+
56
+ def _restore_chat_scroll_if_needed(self, was_at_bottom: bool) -> None:
57
+ # The completion list is a normal grid row, not a true overlay. Showing or
58
+ # hiding it changes the available height for ChatLog. When the chat is
59
+ # already bottom-aligned, that resize can leave the viewport briefly at an
60
+ # intermediate scroll offset and cause a visible flicker. Restore the
61
+ # bottom scroll position after Textual has applied the layout change.
62
+ if was_at_bottom:
63
+ chat = self.query_one("#chat-log", ChatLog)
64
+ chat.scroll_end(animate=False)
65
+
66
+ def _restore_chat_scroll_after_refresh(self, was_at_bottom: bool) -> None:
67
+ self.call_after_refresh(lambda: self._restore_chat_scroll_if_needed(was_at_bottom))
68
+
69
+ def _set_bottom_info_displaced(self, displaced: bool) -> None:
70
+ info_bar = self.query_one("#info-bar", InfoBar)
71
+ if displaced:
72
+ info_bar.add_class("-completion-hidden")
73
+ else:
74
+ info_bar.remove_class("-completion-hidden")
75
+
76
+ def _show_completion_list(
77
+ self,
78
+ items: list[ListItem],
79
+ *,
80
+ searchable: bool = False,
81
+ max_label_width: int | None = None,
82
+ ) -> None:
83
+ completion_list = self.query_one("#completion-list", FloatingList)
84
+ self._set_bottom_info_displaced(True)
85
+ completion_list.show(items, searchable=searchable, max_label_width=max_label_width)
86
+
87
+ def _hide_completion_list(self, *, restore_info_bar: bool = True) -> None:
88
+ completion_list = self.query_one("#completion-list", FloatingList)
89
+ completion_list.hide()
90
+ if restore_info_bar:
91
+ self._set_bottom_info_displaced(False)
92
+
93
+ @on(InputBox.CompletionUpdate)
94
+ def on_completion_update(self, event: InputBox.CompletionUpdate) -> None:
95
+ if self._selection_mode is not None:
96
+ return
97
+
98
+ completion_list = self.query_one("#completion-list", FloatingList)
99
+ was_at_bottom = self._is_chat_at_bottom()
100
+ if completion_list.is_visible:
101
+ self._set_bottom_info_displaced(True)
102
+ completion_list.update_items(event.items)
103
+ else:
104
+ self._show_completion_list(event.items)
105
+ self._restore_chat_scroll_after_refresh(was_at_bottom)
106
+
107
+ @on(InputBox.CompletionHide)
108
+ def on_completion_hide(self, event: InputBox.CompletionHide) -> None:
109
+ input_box = self.query_one("#input-box", InputBox)
110
+ was_at_bottom = self._is_chat_at_bottom()
111
+
112
+ with self.batch_update():
113
+ if self._selection_mode is not None:
114
+ # If we were in a sub-picker from settings, go back to settings
115
+ if self._settings_active:
116
+ self._hide_completion_list(restore_info_bar=False)
117
+ self._settings_active = False
118
+ self._show_settings_picker()
119
+ self._restore_chat_scroll_after_refresh(was_at_bottom)
120
+ return
121
+
122
+ self._hide_completion_list()
123
+ self._selection_mode = None
124
+ input_box.clear()
125
+ input_box.set_autocomplete_enabled(True)
126
+ self._reset_ctrl_d_delete_state()
127
+ else:
128
+ self._hide_completion_list()
129
+
130
+ input_box.set_completing(False)
131
+
132
+ self._restore_chat_scroll_after_refresh(was_at_bottom)
133
+
134
+ @on(InputBox.CompletionSelect)
135
+ def on_completion_select(self, event: InputBox.CompletionSelect) -> None:
136
+ input_box = self.query_one("#input-box", InputBox)
137
+ if self._selection_mode == SelectionMode.TREE:
138
+ self.query_one("#tree-selector", TreeSelector).action_select()
139
+ return
140
+ was_at_bottom = self._is_chat_at_bottom()
141
+ completion_list = self.query_one("#completion-list", FloatingList)
142
+ item = completion_list.selected_item
143
+
144
+ if not item:
145
+ self._hide_completion_list()
146
+ input_box.set_completing(False)
147
+ input_box.submit_raw()
148
+ self._restore_chat_scroll_after_refresh(was_at_bottom)
149
+ return
150
+
151
+ if self._selection_mode is not None:
152
+ self._apply_selection_mode_choice(item, input_box, was_at_bottom)
153
+ return
154
+
155
+ if input_box.is_tab_completing:
156
+ self._hide_completion_list()
157
+ input_box.apply_tab_path_completion(item)
158
+ self._restore_chat_scroll_after_refresh(was_at_bottom)
159
+ return
160
+
161
+ provider = input_box.active_provider
162
+ self._hide_completion_list()
163
+
164
+ if isinstance(provider, SlashCommandProvider):
165
+ input_box.apply_slash_command(item)
166
+ elif isinstance(provider, FilePathProvider | PullRequestProvider):
167
+ input_box.apply_provider_completion(item)
168
+
169
+ input_box.set_completing(False)
170
+ self._restore_chat_scroll_after_refresh(was_at_bottom)
171
+
172
+ def _apply_selection_mode_choice(
173
+ self, item: ListItem, input_box: InputBox, was_at_bottom: bool
174
+ ) -> None:
175
+ """Apply the picked item for the active selection mode (model/theme/session/...)."""
176
+ selection_mode = self._selection_mode
177
+ keeps_info_bar_displaced = selection_mode == SelectionMode.SETTINGS or (
178
+ selection_mode
179
+ in (SelectionMode.THEME, SelectionMode.THINKING, SelectionMode.THINKING_LINES)
180
+ and self._settings_active
181
+ )
182
+ with self.batch_update():
183
+ self._hide_completion_list(restore_info_bar=not keeps_info_bar_displaced)
184
+ self._selection_mode = None
185
+ input_box.clear()
186
+ input_box.set_autocomplete_enabled(True)
187
+ input_box.set_completing(False)
188
+ self._reset_ctrl_d_delete_state()
189
+
190
+ def show_settings_picker_and_restore() -> None:
191
+ self._show_settings_picker()
192
+ self._restore_chat_scroll_after_refresh(was_at_bottom)
193
+
194
+ match selection_mode:
195
+ case SelectionMode.SETTINGS:
196
+ settings_result = self._handle_settings_select(item.value)
197
+ if settings_result == "closed":
198
+ self._set_bottom_info_displaced(False)
199
+ case SelectionMode.SESSION:
200
+ self.run_worker(self._load_session(item.value.path), exclusive=True)
201
+ case SelectionMode.TREE:
202
+ pass
203
+ case SelectionMode.MODEL:
204
+ self._select_model(item.value)
205
+ case SelectionMode.THEME:
206
+ self._select_theme(item.value)
207
+ if self._settings_active:
208
+ self._settings_active = False
209
+ self.call_later(show_settings_picker_and_restore)
210
+ return
211
+ case SelectionMode.PERMISSIONS:
212
+ self._select_permission_mode(item.value)
213
+ case SelectionMode.THINKING:
214
+ self._select_thinking_level(item.value)
215
+ if self._settings_active:
216
+ self._settings_active = False
217
+ self.call_later(show_settings_picker_and_restore)
218
+ return
219
+ case SelectionMode.THINKING_LINES:
220
+ self._select_thinking_lines(item.value)
221
+ if self._settings_active:
222
+ self._settings_active = False
223
+ self.call_later(show_settings_picker_and_restore)
224
+ return
225
+ case SelectionMode.NOTIFICATIONS:
226
+ self._select_notifications_mode(item.value)
227
+ case SelectionMode.LOGIN:
228
+ self._select_login_provider(item.value)
229
+ case SelectionMode.LOGOUT:
230
+ self._select_logout_provider(item.value)
231
+ case SelectionMode.API_KEY_ACTION:
232
+ self._select_api_key_action(item.value)
233
+
234
+ self._restore_chat_scroll_after_refresh(was_at_bottom)
235
+
236
+ @on(InputBox.SearchUpdate)
237
+ def on_search_update(self, event: InputBox.SearchUpdate) -> None:
238
+ if self._selection_mode is None or self._selection_mode == SelectionMode.TREE:
239
+ return
240
+ completion_list = self.query_one("#completion-list", FloatingList)
241
+ completion_list.set_search_query(event.query)
242
+ if (
243
+ self._selection_mode == SelectionMode.SETTINGS
244
+ and not event.query
245
+ and self._settings_selected_value is not None
246
+ ):
247
+ completion_list.select_value(self._settings_selected_value)
248
+
249
+ @on(InputBox.CompletionMove)
250
+ def on_completion_move(self, event: InputBox.CompletionMove) -> None:
251
+ if self._selection_mode == SelectionMode.TREE:
252
+ selector = self.query_one("#tree-selector", TreeSelector)
253
+ if event.direction < 0:
254
+ selector.action_move_up()
255
+ else:
256
+ selector.action_move_down()
257
+ return
258
+ completion_list = self.query_one("#completion-list", FloatingList)
259
+ if event.direction < 0:
260
+ completion_list.move_up()
261
+ else:
262
+ completion_list.move_down()
263
+
264
+ @on(TreeSelector.Selected)
265
+ async def on_tree_selected(self, event: TreeSelector.Selected) -> None:
266
+ selector = self.query_one("#tree-selector", TreeSelector)
267
+ input_box = self.query_one("#input-box", InputBox)
268
+ chat = self.query_one("#chat-log", ChatLog)
269
+ info_bar = self.query_one("#info-bar", InfoBar)
270
+ status = self.query_one("#status-line", StatusLine)
271
+ try:
272
+ result = self._runtime.navigate_tree(event.entry_id)
273
+ except Exception as exc:
274
+ chat.add_info_message(f"Tree navigation failed: {exc}", error=True)
275
+ return
276
+ selector.hide()
277
+ self._selection_mode = None
278
+ input_box.set_autocomplete_enabled(True)
279
+ input_box.set_completing(False)
280
+ await chat.remove_all_children()
281
+ chat.add_session_info(getattr(self, "VERSION", ""))
282
+ if self._runtime.context:
283
+ chat.add_loaded_resources(
284
+ context_paths=[format_path(f.path) for f in self._runtime.context.agents_files],
285
+ skills=self._runtime.context.skills,
286
+ tools=self._runtime.tools,
287
+ )
288
+ if self._runtime.session:
289
+ self._render_session_entries(self._runtime.session)
290
+ totals = self._runtime.session.token_totals()
291
+ info_bar.set_tokens(
292
+ totals.input_tokens,
293
+ totals.output_tokens,
294
+ totals.context_tokens,
295
+ totals.cache_read_tokens,
296
+ totals.cache_write_tokens,
297
+ )
298
+ info_bar.set_file_changes(self._runtime.session.file_changes_summary())
299
+ status.reset()
300
+ if result.editor_text and not input_box.text.strip():
301
+ input_box.insert(result.editor_text)
302
+ chat.show_status("Navigated to selected point")
303
+ input_box.focus()
304
+
305
+ @on(TreeSelector.Cancelled)
306
+ def on_tree_cancelled(self, event: TreeSelector.Cancelled) -> None:
307
+ selector = self.query_one("#tree-selector", TreeSelector)
308
+ input_box = self.query_one("#input-box", InputBox)
309
+ selector.hide()
310
+ self._selection_mode = None
311
+ input_box.set_autocomplete_enabled(True)
312
+ input_box.set_completing(False)
313
+ input_box.focus()