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/completion_ui.py
ADDED
|
@@ -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()
|