codemaster-cli 2.2.0__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.
- codemaster_cli-2.2.0.dist-info/METADATA +645 -0
- codemaster_cli-2.2.0.dist-info/RECORD +170 -0
- codemaster_cli-2.2.0.dist-info/WHEEL +4 -0
- codemaster_cli-2.2.0.dist-info/entry_points.txt +3 -0
- vibe/__init__.py +6 -0
- vibe/acp/__init__.py +0 -0
- vibe/acp/acp_agent_loop.py +746 -0
- vibe/acp/entrypoint.py +81 -0
- vibe/acp/tools/__init__.py +0 -0
- vibe/acp/tools/base.py +100 -0
- vibe/acp/tools/builtins/bash.py +134 -0
- vibe/acp/tools/builtins/read_file.py +54 -0
- vibe/acp/tools/builtins/search_replace.py +129 -0
- vibe/acp/tools/builtins/todo.py +65 -0
- vibe/acp/tools/builtins/write_file.py +98 -0
- vibe/acp/tools/session_update.py +118 -0
- vibe/acp/utils.py +213 -0
- vibe/cli/__init__.py +0 -0
- vibe/cli/autocompletion/__init__.py +0 -0
- vibe/cli/autocompletion/base.py +22 -0
- vibe/cli/autocompletion/path_completion.py +177 -0
- vibe/cli/autocompletion/slash_command.py +99 -0
- vibe/cli/cli.py +188 -0
- vibe/cli/clipboard.py +69 -0
- vibe/cli/commands.py +116 -0
- vibe/cli/entrypoint.py +163 -0
- vibe/cli/history_manager.py +91 -0
- vibe/cli/plan_offer/adapters/http_whoami_gateway.py +67 -0
- vibe/cli/plan_offer/decide_plan_offer.py +87 -0
- vibe/cli/plan_offer/ports/whoami_gateway.py +23 -0
- vibe/cli/terminal_setup.py +323 -0
- vibe/cli/textual_ui/__init__.py +0 -0
- vibe/cli/textual_ui/ansi_markdown.py +58 -0
- vibe/cli/textual_ui/app.py +1546 -0
- vibe/cli/textual_ui/app.tcss +1020 -0
- vibe/cli/textual_ui/external_editor.py +32 -0
- vibe/cli/textual_ui/handlers/__init__.py +5 -0
- vibe/cli/textual_ui/handlers/event_handler.py +147 -0
- vibe/cli/textual_ui/widgets/__init__.py +0 -0
- vibe/cli/textual_ui/widgets/approval_app.py +192 -0
- vibe/cli/textual_ui/widgets/banner/banner.py +85 -0
- vibe/cli/textual_ui/widgets/banner/petit_chat.py +195 -0
- vibe/cli/textual_ui/widgets/braille_renderer.py +58 -0
- vibe/cli/textual_ui/widgets/chat_input/__init__.py +7 -0
- vibe/cli/textual_ui/widgets/chat_input/body.py +214 -0
- vibe/cli/textual_ui/widgets/chat_input/completion_manager.py +58 -0
- vibe/cli/textual_ui/widgets/chat_input/completion_popup.py +43 -0
- vibe/cli/textual_ui/widgets/chat_input/container.py +195 -0
- vibe/cli/textual_ui/widgets/chat_input/text_area.py +365 -0
- vibe/cli/textual_ui/widgets/compact.py +41 -0
- vibe/cli/textual_ui/widgets/config_app.py +171 -0
- vibe/cli/textual_ui/widgets/context_progress.py +30 -0
- vibe/cli/textual_ui/widgets/load_more.py +43 -0
- vibe/cli/textual_ui/widgets/loading.py +201 -0
- vibe/cli/textual_ui/widgets/messages.py +277 -0
- vibe/cli/textual_ui/widgets/no_markup_static.py +11 -0
- vibe/cli/textual_ui/widgets/path_display.py +28 -0
- vibe/cli/textual_ui/widgets/proxy_setup_app.py +127 -0
- vibe/cli/textual_ui/widgets/question_app.py +496 -0
- vibe/cli/textual_ui/widgets/spinner.py +194 -0
- vibe/cli/textual_ui/widgets/status_message.py +76 -0
- vibe/cli/textual_ui/widgets/teleport_message.py +31 -0
- vibe/cli/textual_ui/widgets/tool_widgets.py +371 -0
- vibe/cli/textual_ui/widgets/tools.py +201 -0
- vibe/cli/textual_ui/windowing/__init__.py +29 -0
- vibe/cli/textual_ui/windowing/history.py +105 -0
- vibe/cli/textual_ui/windowing/history_windowing.py +71 -0
- vibe/cli/textual_ui/windowing/state.py +105 -0
- vibe/cli/update_notifier/__init__.py +47 -0
- vibe/cli/update_notifier/adapters/filesystem_update_cache_repository.py +59 -0
- vibe/cli/update_notifier/adapters/github_update_gateway.py +101 -0
- vibe/cli/update_notifier/adapters/pypi_update_gateway.py +107 -0
- vibe/cli/update_notifier/ports/update_cache_repository.py +16 -0
- vibe/cli/update_notifier/ports/update_gateway.py +53 -0
- vibe/cli/update_notifier/update.py +139 -0
- vibe/cli/update_notifier/whats_new.py +49 -0
- vibe/core/__init__.py +5 -0
- vibe/core/agent_loop.py +1075 -0
- vibe/core/agents/__init__.py +31 -0
- vibe/core/agents/manager.py +165 -0
- vibe/core/agents/models.py +122 -0
- vibe/core/auth/__init__.py +6 -0
- vibe/core/auth/crypto.py +137 -0
- vibe/core/auth/github.py +178 -0
- vibe/core/autocompletion/__init__.py +0 -0
- vibe/core/autocompletion/completers.py +257 -0
- vibe/core/autocompletion/file_indexer/__init__.py +10 -0
- vibe/core/autocompletion/file_indexer/ignore_rules.py +156 -0
- vibe/core/autocompletion/file_indexer/indexer.py +179 -0
- vibe/core/autocompletion/file_indexer/store.py +169 -0
- vibe/core/autocompletion/file_indexer/watcher.py +71 -0
- vibe/core/autocompletion/fuzzy.py +189 -0
- vibe/core/autocompletion/path_prompt.py +108 -0
- vibe/core/autocompletion/path_prompt_adapter.py +149 -0
- vibe/core/config.py +673 -0
- vibe/core/config_PATCH_INSTRUCTIONS.md +77 -0
- vibe/core/llm/__init__.py +0 -0
- vibe/core/llm/backend/anthropic.py +630 -0
- vibe/core/llm/backend/base.py +38 -0
- vibe/core/llm/backend/factory.py +7 -0
- vibe/core/llm/backend/generic.py +425 -0
- vibe/core/llm/backend/mistral.py +381 -0
- vibe/core/llm/backend/vertex.py +115 -0
- vibe/core/llm/exceptions.py +195 -0
- vibe/core/llm/format.py +184 -0
- vibe/core/llm/message_utils.py +24 -0
- vibe/core/llm/types.py +120 -0
- vibe/core/middleware.py +209 -0
- vibe/core/output_formatters.py +85 -0
- vibe/core/paths/__init__.py +0 -0
- vibe/core/paths/config_paths.py +68 -0
- vibe/core/paths/global_paths.py +40 -0
- vibe/core/programmatic.py +56 -0
- vibe/core/prompts/__init__.py +32 -0
- vibe/core/prompts/cli.md +111 -0
- vibe/core/prompts/compact.md +48 -0
- vibe/core/prompts/dangerous_directory.md +5 -0
- vibe/core/prompts/explore.md +50 -0
- vibe/core/prompts/project_context.md +8 -0
- vibe/core/prompts/tests.md +1 -0
- vibe/core/proxy_setup.py +65 -0
- vibe/core/session/session_loader.py +222 -0
- vibe/core/session/session_logger.py +318 -0
- vibe/core/session/session_migration.py +41 -0
- vibe/core/skills/__init__.py +7 -0
- vibe/core/skills/manager.py +132 -0
- vibe/core/skills/models.py +92 -0
- vibe/core/skills/parser.py +39 -0
- vibe/core/system_prompt.py +466 -0
- vibe/core/telemetry/__init__.py +0 -0
- vibe/core/telemetry/send.py +185 -0
- vibe/core/teleport/errors.py +9 -0
- vibe/core/teleport/git.py +196 -0
- vibe/core/teleport/nuage.py +180 -0
- vibe/core/teleport/teleport.py +208 -0
- vibe/core/teleport/types.py +54 -0
- vibe/core/tools/base.py +336 -0
- vibe/core/tools/builtins/ask_user_question.py +134 -0
- vibe/core/tools/builtins/bash.py +357 -0
- vibe/core/tools/builtins/grep.py +310 -0
- vibe/core/tools/builtins/prompts/__init__.py +0 -0
- vibe/core/tools/builtins/prompts/ask_user_question.md +84 -0
- vibe/core/tools/builtins/prompts/bash.md +73 -0
- vibe/core/tools/builtins/prompts/grep.md +4 -0
- vibe/core/tools/builtins/prompts/read_file.md +13 -0
- vibe/core/tools/builtins/prompts/search_replace.md +43 -0
- vibe/core/tools/builtins/prompts/task.md +24 -0
- vibe/core/tools/builtins/prompts/todo.md +199 -0
- vibe/core/tools/builtins/prompts/write_file.md +42 -0
- vibe/core/tools/builtins/read_file.py +222 -0
- vibe/core/tools/builtins/search_replace.py +456 -0
- vibe/core/tools/builtins/task.py +154 -0
- vibe/core/tools/builtins/todo.py +134 -0
- vibe/core/tools/builtins/write_file.py +160 -0
- vibe/core/tools/manager.py +341 -0
- vibe/core/tools/mcp.py +397 -0
- vibe/core/tools/ui.py +68 -0
- vibe/core/trusted_folders.py +86 -0
- vibe/core/types.py +405 -0
- vibe/core/utils.py +396 -0
- vibe/setup/onboarding/__init__.py +39 -0
- vibe/setup/onboarding/base.py +14 -0
- vibe/setup/onboarding/onboarding.tcss +134 -0
- vibe/setup/onboarding/screens/__init__.py +5 -0
- vibe/setup/onboarding/screens/api_key.py +200 -0
- vibe/setup/onboarding/screens/provider_selection.py +87 -0
- vibe/setup/onboarding/screens/welcome.py +136 -0
- vibe/setup/trusted_folders/trust_folder_dialog.py +180 -0
- vibe/setup/trusted_folders/trust_folder_dialog.tcss +83 -0
- vibe/whats_new.md +5 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, ClassVar, Literal
|
|
4
|
+
|
|
5
|
+
from textual import events
|
|
6
|
+
from textual.binding import Binding
|
|
7
|
+
from textual.message import Message
|
|
8
|
+
from textual.widgets import TextArea
|
|
9
|
+
|
|
10
|
+
from vibe.cli.autocompletion.base import CompletionResult
|
|
11
|
+
from vibe.cli.textual_ui.external_editor import ExternalEditor
|
|
12
|
+
from vibe.cli.textual_ui.widgets.chat_input.completion_manager import (
|
|
13
|
+
MultiCompletionManager,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
InputMode = Literal["!", "/", ">", "&"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ChatTextArea(TextArea):
|
|
20
|
+
BINDINGS: ClassVar[list[Binding]] = [
|
|
21
|
+
Binding(
|
|
22
|
+
"shift+enter,ctrl+j",
|
|
23
|
+
"insert_newline",
|
|
24
|
+
"New Line",
|
|
25
|
+
show=False,
|
|
26
|
+
priority=True,
|
|
27
|
+
),
|
|
28
|
+
Binding("ctrl+g", "open_external_editor", "External Editor", show=False),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
DEFAULT_MODE: ClassVar[Literal[">"]] = ">"
|
|
32
|
+
|
|
33
|
+
class Submitted(Message):
|
|
34
|
+
def __init__(self, value: str) -> None:
|
|
35
|
+
self.value = value
|
|
36
|
+
super().__init__()
|
|
37
|
+
|
|
38
|
+
class HistoryPrevious(Message):
|
|
39
|
+
def __init__(self, prefix: str) -> None:
|
|
40
|
+
self.prefix = prefix
|
|
41
|
+
super().__init__()
|
|
42
|
+
|
|
43
|
+
class HistoryNext(Message):
|
|
44
|
+
def __init__(self, prefix: str) -> None:
|
|
45
|
+
self.prefix = prefix
|
|
46
|
+
super().__init__()
|
|
47
|
+
|
|
48
|
+
class HistoryReset(Message):
|
|
49
|
+
"""Message sent when history navigation should be reset."""
|
|
50
|
+
|
|
51
|
+
class ModeChanged(Message):
|
|
52
|
+
"""Message sent when the input mode changes (>, !, /, &)."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, mode: InputMode) -> None:
|
|
55
|
+
self.mode = mode
|
|
56
|
+
super().__init__()
|
|
57
|
+
|
|
58
|
+
def __init__(self, nuage_enabled: bool = False, **kwargs: Any) -> None:
|
|
59
|
+
super().__init__(**kwargs)
|
|
60
|
+
self._nuage_enabled = nuage_enabled
|
|
61
|
+
self._input_mode: InputMode = self.DEFAULT_MODE
|
|
62
|
+
self._history_prefix: str | None = None
|
|
63
|
+
self._last_text = ""
|
|
64
|
+
self._navigating_history = False
|
|
65
|
+
self._last_cursor_col: int = 0
|
|
66
|
+
self._last_used_prefix: str | None = None
|
|
67
|
+
self._original_text: str = ""
|
|
68
|
+
self._cursor_pos_after_load: tuple[int, int] | None = None
|
|
69
|
+
self._cursor_moved_since_load: bool = False
|
|
70
|
+
self._completion_manager: MultiCompletionManager | None = None
|
|
71
|
+
self._app_has_focus: bool = True
|
|
72
|
+
|
|
73
|
+
def on_blur(self, event: events.Blur) -> None:
|
|
74
|
+
if self._app_has_focus:
|
|
75
|
+
self.call_after_refresh(self.focus)
|
|
76
|
+
|
|
77
|
+
def set_app_focus(self, has_focus: bool) -> None:
|
|
78
|
+
self._app_has_focus = has_focus
|
|
79
|
+
self.cursor_blink = has_focus
|
|
80
|
+
if has_focus and not self.has_focus:
|
|
81
|
+
self.call_after_refresh(self.focus)
|
|
82
|
+
|
|
83
|
+
def on_click(self, event: events.Click) -> None:
|
|
84
|
+
self._mark_cursor_moved_if_needed()
|
|
85
|
+
|
|
86
|
+
def action_insert_newline(self) -> None:
|
|
87
|
+
self.insert("\n")
|
|
88
|
+
|
|
89
|
+
def action_open_external_editor(self) -> None:
|
|
90
|
+
editor = ExternalEditor()
|
|
91
|
+
current_text = self.get_full_text()
|
|
92
|
+
|
|
93
|
+
with self.app.suspend():
|
|
94
|
+
result = editor.edit(current_text)
|
|
95
|
+
|
|
96
|
+
if result is not None:
|
|
97
|
+
self.clear()
|
|
98
|
+
self.insert(result)
|
|
99
|
+
|
|
100
|
+
def on_text_area_changed(self, event: TextArea.Changed) -> None:
|
|
101
|
+
if not self._navigating_history and self.text != self._last_text:
|
|
102
|
+
self._reset_prefix()
|
|
103
|
+
self._original_text = ""
|
|
104
|
+
self._cursor_pos_after_load = None
|
|
105
|
+
self._cursor_moved_since_load = False
|
|
106
|
+
self.post_message(self.HistoryReset())
|
|
107
|
+
self._last_text = self.text
|
|
108
|
+
was_navigating_history = self._navigating_history
|
|
109
|
+
self._navigating_history = False
|
|
110
|
+
|
|
111
|
+
if self._completion_manager and not was_navigating_history:
|
|
112
|
+
self._completion_manager.on_text_changed(
|
|
113
|
+
self.get_full_text(), self._get_full_cursor_offset()
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _reset_prefix(self) -> None:
|
|
117
|
+
self._history_prefix = None
|
|
118
|
+
self._last_used_prefix = None
|
|
119
|
+
|
|
120
|
+
def _mark_cursor_moved_if_needed(self) -> None:
|
|
121
|
+
if (
|
|
122
|
+
self._cursor_pos_after_load is not None
|
|
123
|
+
and not self._cursor_moved_since_load
|
|
124
|
+
and self.cursor_location != self._cursor_pos_after_load
|
|
125
|
+
):
|
|
126
|
+
self._cursor_moved_since_load = True
|
|
127
|
+
self._reset_prefix()
|
|
128
|
+
|
|
129
|
+
def _get_prefix_up_to_cursor(self) -> str:
|
|
130
|
+
cursor_row, cursor_col = self.cursor_location
|
|
131
|
+
lines = self.text.split("\n")
|
|
132
|
+
if cursor_row < len(lines):
|
|
133
|
+
visible_prefix = lines[cursor_row][:cursor_col]
|
|
134
|
+
if cursor_row == 0 and self._input_mode != self.DEFAULT_MODE:
|
|
135
|
+
return self._input_mode + visible_prefix
|
|
136
|
+
return visible_prefix
|
|
137
|
+
return ""
|
|
138
|
+
|
|
139
|
+
def _handle_history_up(self) -> bool:
|
|
140
|
+
cursor_row, cursor_col = self.cursor_location
|
|
141
|
+
if cursor_row == 0:
|
|
142
|
+
if self._history_prefix is not None and cursor_col != self._last_cursor_col:
|
|
143
|
+
self._reset_prefix()
|
|
144
|
+
self._last_cursor_col = 0
|
|
145
|
+
|
|
146
|
+
if self._history_prefix is None:
|
|
147
|
+
self._history_prefix = self._get_prefix_up_to_cursor()
|
|
148
|
+
|
|
149
|
+
self._navigating_history = True
|
|
150
|
+
self.post_message(self.HistoryPrevious(self._history_prefix))
|
|
151
|
+
return True
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
def _handle_history_down(self) -> bool:
|
|
155
|
+
cursor_row, cursor_col = self.cursor_location
|
|
156
|
+
total_lines = self.text.count("\n") + 1
|
|
157
|
+
|
|
158
|
+
on_first_line_unmoved = cursor_row == 0 and not self._cursor_moved_since_load
|
|
159
|
+
on_last_line = cursor_row == total_lines - 1
|
|
160
|
+
|
|
161
|
+
should_intercept = (
|
|
162
|
+
on_first_line_unmoved and self._history_prefix is not None
|
|
163
|
+
) or on_last_line
|
|
164
|
+
|
|
165
|
+
if not should_intercept:
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
if self._history_prefix is not None and cursor_col != self._last_cursor_col:
|
|
169
|
+
self._reset_prefix()
|
|
170
|
+
self._last_cursor_col = 0
|
|
171
|
+
|
|
172
|
+
if self._history_prefix is None:
|
|
173
|
+
self._history_prefix = self._get_prefix_up_to_cursor()
|
|
174
|
+
|
|
175
|
+
self._navigating_history = True
|
|
176
|
+
self.post_message(self.HistoryNext(self._history_prefix))
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
async def _on_key(self, event: events.Key) -> None: # noqa: PLR0911
|
|
180
|
+
self._mark_cursor_moved_if_needed()
|
|
181
|
+
|
|
182
|
+
manager = self._completion_manager
|
|
183
|
+
if manager:
|
|
184
|
+
match manager.on_key(
|
|
185
|
+
event, self.get_full_text(), self._get_full_cursor_offset()
|
|
186
|
+
):
|
|
187
|
+
case CompletionResult.HANDLED:
|
|
188
|
+
event.prevent_default()
|
|
189
|
+
event.stop()
|
|
190
|
+
return
|
|
191
|
+
case CompletionResult.SUBMIT:
|
|
192
|
+
event.prevent_default()
|
|
193
|
+
event.stop()
|
|
194
|
+
value = self.get_full_text().strip()
|
|
195
|
+
if value:
|
|
196
|
+
self._reset_prefix()
|
|
197
|
+
self.post_message(self.Submitted(value))
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
if event.key == "enter":
|
|
201
|
+
event.prevent_default()
|
|
202
|
+
event.stop()
|
|
203
|
+
value = self.get_full_text().strip()
|
|
204
|
+
if value:
|
|
205
|
+
self._reset_prefix()
|
|
206
|
+
self.post_message(self.Submitted(value))
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
if event.key == "shift+enter":
|
|
210
|
+
event.prevent_default()
|
|
211
|
+
event.stop()
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
if (
|
|
215
|
+
event.character
|
|
216
|
+
and event.character in self.mode_characters
|
|
217
|
+
and not self.text
|
|
218
|
+
and self._input_mode == self.DEFAULT_MODE
|
|
219
|
+
):
|
|
220
|
+
self._set_mode(event.character)
|
|
221
|
+
event.prevent_default()
|
|
222
|
+
event.stop()
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
if event.key == "backspace" and self._should_reset_mode_on_backspace():
|
|
226
|
+
self._set_mode(self.DEFAULT_MODE)
|
|
227
|
+
event.prevent_default()
|
|
228
|
+
event.stop()
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
if event.key == "up" and self._handle_history_up():
|
|
232
|
+
event.prevent_default()
|
|
233
|
+
event.stop()
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
if event.key == "down" and self._handle_history_down():
|
|
237
|
+
event.prevent_default()
|
|
238
|
+
event.stop()
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
await super()._on_key(event)
|
|
242
|
+
self._mark_cursor_moved_if_needed()
|
|
243
|
+
|
|
244
|
+
def set_completion_manager(self, manager: MultiCompletionManager | None) -> None:
|
|
245
|
+
self._completion_manager = manager
|
|
246
|
+
if self._completion_manager:
|
|
247
|
+
self._completion_manager.on_text_changed(
|
|
248
|
+
self.get_full_text(), self._get_full_cursor_offset()
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def get_cursor_offset(self) -> int:
|
|
252
|
+
text = self.text
|
|
253
|
+
row, col = self.cursor_location
|
|
254
|
+
|
|
255
|
+
if not text:
|
|
256
|
+
return 0
|
|
257
|
+
|
|
258
|
+
lines = text.split("\n")
|
|
259
|
+
row = max(0, min(row, len(lines) - 1))
|
|
260
|
+
col = max(0, col)
|
|
261
|
+
|
|
262
|
+
offset = sum(len(lines[i]) + 1 for i in range(row))
|
|
263
|
+
return offset + min(col, len(lines[row]))
|
|
264
|
+
|
|
265
|
+
def set_cursor_offset(self, offset: int) -> None:
|
|
266
|
+
text = self.text
|
|
267
|
+
if offset <= 0:
|
|
268
|
+
self.move_cursor((0, 0))
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
if offset >= len(text):
|
|
272
|
+
lines = text.split("\n")
|
|
273
|
+
if not lines:
|
|
274
|
+
self.move_cursor((0, 0))
|
|
275
|
+
return
|
|
276
|
+
last_row = len(lines) - 1
|
|
277
|
+
self.move_cursor((last_row, len(lines[last_row])))
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
remaining = offset
|
|
281
|
+
lines = text.split("\n")
|
|
282
|
+
|
|
283
|
+
for row, line in enumerate(lines):
|
|
284
|
+
line_length = len(line)
|
|
285
|
+
if remaining <= line_length:
|
|
286
|
+
self.move_cursor((row, remaining))
|
|
287
|
+
return
|
|
288
|
+
remaining -= line_length + 1
|
|
289
|
+
|
|
290
|
+
last_row = len(lines) - 1
|
|
291
|
+
self.move_cursor((last_row, len(lines[last_row])))
|
|
292
|
+
|
|
293
|
+
def reset_history_state(self) -> None:
|
|
294
|
+
self._reset_prefix()
|
|
295
|
+
self._original_text = ""
|
|
296
|
+
self._cursor_pos_after_load = None
|
|
297
|
+
self._cursor_moved_since_load = False
|
|
298
|
+
self._last_text = self.text
|
|
299
|
+
|
|
300
|
+
def clear_text(self) -> None:
|
|
301
|
+
self.clear()
|
|
302
|
+
self.reset_history_state()
|
|
303
|
+
self._set_mode(self.DEFAULT_MODE)
|
|
304
|
+
|
|
305
|
+
def _set_mode(self, mode: InputMode) -> None:
|
|
306
|
+
if self._input_mode == mode:
|
|
307
|
+
return
|
|
308
|
+
self._input_mode = mode
|
|
309
|
+
self.post_message(self.ModeChanged(mode))
|
|
310
|
+
if self._completion_manager:
|
|
311
|
+
self._completion_manager.on_text_changed(
|
|
312
|
+
self.get_full_text(), self._get_full_cursor_offset()
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def _should_reset_mode_on_backspace(self) -> bool:
|
|
316
|
+
return (
|
|
317
|
+
self._input_mode != self.DEFAULT_MODE
|
|
318
|
+
and not self.text
|
|
319
|
+
and self.get_cursor_offset() == 0
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
def get_full_text(self) -> str:
|
|
323
|
+
if self._input_mode != self.DEFAULT_MODE:
|
|
324
|
+
return self._input_mode + self.text
|
|
325
|
+
return self.text
|
|
326
|
+
|
|
327
|
+
def _get_full_cursor_offset(self) -> int:
|
|
328
|
+
return self.get_cursor_offset() + self._get_mode_prefix_length()
|
|
329
|
+
|
|
330
|
+
def _get_mode_prefix_length(self) -> int:
|
|
331
|
+
return {">": 0, "/": 1, "!": 1, "&": 1}[self._input_mode]
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def mode_characters(self) -> set[InputMode]:
|
|
335
|
+
chars: set[InputMode] = {"!", "/"}
|
|
336
|
+
if self._nuage_enabled:
|
|
337
|
+
chars.add("&")
|
|
338
|
+
return chars
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def input_mode(self) -> InputMode:
|
|
342
|
+
return self._input_mode
|
|
343
|
+
|
|
344
|
+
def set_mode(self, mode: InputMode) -> None:
|
|
345
|
+
if self._input_mode != mode:
|
|
346
|
+
self._input_mode = mode
|
|
347
|
+
self.post_message(self.ModeChanged(mode))
|
|
348
|
+
|
|
349
|
+
def adjust_from_full_text_coords(
|
|
350
|
+
self, start: int, end: int, replacement: str
|
|
351
|
+
) -> tuple[int, int, str]:
|
|
352
|
+
"""Translate from full-text coordinates to widget coordinates.
|
|
353
|
+
|
|
354
|
+
The completion manager works with 'full text' that includes the mode prefix.
|
|
355
|
+
This adjusts coordinates and replacement text for the actual widget text.
|
|
356
|
+
"""
|
|
357
|
+
mode_len = self._get_mode_prefix_length()
|
|
358
|
+
|
|
359
|
+
adj_start = max(0, start - mode_len)
|
|
360
|
+
adj_end = max(adj_start, end - mode_len)
|
|
361
|
+
|
|
362
|
+
if mode_len > 0 and replacement.startswith(self._input_mode):
|
|
363
|
+
replacement = replacement[mode_len:]
|
|
364
|
+
|
|
365
|
+
return adj_start, adj_end, replacement
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from textual.message import Message
|
|
4
|
+
|
|
5
|
+
from vibe.cli.textual_ui.widgets.status_message import StatusMessage
|
|
6
|
+
from vibe.core.utils import compact_reduction_display
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CompactMessage(StatusMessage):
|
|
10
|
+
class Completed(Message):
|
|
11
|
+
def __init__(self, compact_widget: CompactMessage) -> None:
|
|
12
|
+
super().__init__()
|
|
13
|
+
self.compact_widget = compact_widget
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
super().__init__()
|
|
17
|
+
self.add_class("compact-message")
|
|
18
|
+
self.old_tokens: int | None = None
|
|
19
|
+
self.new_tokens: int | None = None
|
|
20
|
+
self.error_message: str | None = None
|
|
21
|
+
|
|
22
|
+
def get_content(self) -> str:
|
|
23
|
+
if self._is_spinning:
|
|
24
|
+
return "Compacting conversation history..."
|
|
25
|
+
|
|
26
|
+
if self.error_message:
|
|
27
|
+
return f"Error: {self.error_message}"
|
|
28
|
+
|
|
29
|
+
return compact_reduction_display(self.old_tokens, self.new_tokens)
|
|
30
|
+
|
|
31
|
+
def set_complete(
|
|
32
|
+
self, old_tokens: int | None = None, new_tokens: int | None = None
|
|
33
|
+
) -> None:
|
|
34
|
+
self.old_tokens = old_tokens
|
|
35
|
+
self.new_tokens = new_tokens
|
|
36
|
+
self.stop_spinning(success=True)
|
|
37
|
+
self.post_message(self.Completed(self))
|
|
38
|
+
|
|
39
|
+
def set_error(self, error_message: str) -> None:
|
|
40
|
+
self.error_message = error_message
|
|
41
|
+
self.stop_spinning(success=False)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, ClassVar, TypedDict
|
|
4
|
+
|
|
5
|
+
from textual import events
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.binding import Binding, BindingType
|
|
8
|
+
from textual.containers import Container, Vertical
|
|
9
|
+
from textual.message import Message
|
|
10
|
+
from textual.widgets import Static
|
|
11
|
+
|
|
12
|
+
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from vibe.core.config import VibeConfig
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SettingDefinition(TypedDict):
|
|
19
|
+
key: str
|
|
20
|
+
label: str
|
|
21
|
+
type: str
|
|
22
|
+
options: list[str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConfigApp(Container):
|
|
26
|
+
can_focus = True
|
|
27
|
+
can_focus_children = False
|
|
28
|
+
|
|
29
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
30
|
+
Binding("up", "move_up", "Up", show=False),
|
|
31
|
+
Binding("down", "move_down", "Down", show=False),
|
|
32
|
+
Binding("space", "toggle_setting", "Toggle", show=False),
|
|
33
|
+
Binding("enter", "cycle", "Next", show=False),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
class SettingChanged(Message):
|
|
37
|
+
def __init__(self, key: str, value: str) -> None:
|
|
38
|
+
super().__init__()
|
|
39
|
+
self.key = key
|
|
40
|
+
self.value = value
|
|
41
|
+
|
|
42
|
+
class ConfigClosed(Message):
|
|
43
|
+
def __init__(self, changes: dict[str, str | bool]) -> None:
|
|
44
|
+
super().__init__()
|
|
45
|
+
self.changes = changes
|
|
46
|
+
|
|
47
|
+
def __init__(self, config: VibeConfig) -> None:
|
|
48
|
+
super().__init__(id="config-app")
|
|
49
|
+
self.config = config
|
|
50
|
+
self.selected_index = 0
|
|
51
|
+
self.changes: dict[str, str] = {}
|
|
52
|
+
|
|
53
|
+
self.settings: list[SettingDefinition] = [
|
|
54
|
+
{
|
|
55
|
+
"key": "active_model",
|
|
56
|
+
"label": "Model",
|
|
57
|
+
"type": "cycle",
|
|
58
|
+
"options": [m.alias for m in self.config.models],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"key": "autocopy_to_clipboard",
|
|
62
|
+
"label": "Auto-copy",
|
|
63
|
+
"type": "cycle",
|
|
64
|
+
"options": ["On", "Off"],
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
self.title_widget: Static | None = None
|
|
69
|
+
self.setting_widgets: list[Static] = []
|
|
70
|
+
self.help_widget: Static | None = None
|
|
71
|
+
|
|
72
|
+
def compose(self) -> ComposeResult:
|
|
73
|
+
with Vertical(id="config-content"):
|
|
74
|
+
self.title_widget = NoMarkupStatic("Settings", classes="settings-title")
|
|
75
|
+
yield self.title_widget
|
|
76
|
+
|
|
77
|
+
yield NoMarkupStatic("")
|
|
78
|
+
|
|
79
|
+
for _ in self.settings:
|
|
80
|
+
widget = NoMarkupStatic("", classes="settings-option")
|
|
81
|
+
self.setting_widgets.append(widget)
|
|
82
|
+
yield widget
|
|
83
|
+
|
|
84
|
+
yield NoMarkupStatic("")
|
|
85
|
+
|
|
86
|
+
self.help_widget = NoMarkupStatic(
|
|
87
|
+
"↑↓ navigate Space/Enter toggle ESC exit", classes="settings-help"
|
|
88
|
+
)
|
|
89
|
+
yield self.help_widget
|
|
90
|
+
|
|
91
|
+
def on_mount(self) -> None:
|
|
92
|
+
self._update_display()
|
|
93
|
+
self.focus()
|
|
94
|
+
|
|
95
|
+
def _get_display_value(self, setting: SettingDefinition) -> str:
|
|
96
|
+
key = setting["key"]
|
|
97
|
+
if key in self.changes:
|
|
98
|
+
return self.changes[key]
|
|
99
|
+
raw_value = getattr(self.config, key, "")
|
|
100
|
+
if isinstance(raw_value, bool):
|
|
101
|
+
return "On" if raw_value else "Off"
|
|
102
|
+
return str(raw_value)
|
|
103
|
+
|
|
104
|
+
def _update_display(self) -> None:
|
|
105
|
+
for i, (setting, widget) in enumerate(
|
|
106
|
+
zip(self.settings, self.setting_widgets, strict=True)
|
|
107
|
+
):
|
|
108
|
+
is_selected = i == self.selected_index
|
|
109
|
+
cursor = "› " if is_selected else " "
|
|
110
|
+
|
|
111
|
+
label: str = setting["label"]
|
|
112
|
+
value: str = self._get_display_value(setting)
|
|
113
|
+
|
|
114
|
+
text = f"{cursor}{label}: {value}"
|
|
115
|
+
|
|
116
|
+
widget.update(text)
|
|
117
|
+
|
|
118
|
+
widget.remove_class("settings-cursor-selected")
|
|
119
|
+
widget.remove_class("settings-value-cycle-selected")
|
|
120
|
+
widget.remove_class("settings-value-cycle-unselected")
|
|
121
|
+
|
|
122
|
+
if is_selected:
|
|
123
|
+
widget.add_class("settings-value-cycle-selected")
|
|
124
|
+
else:
|
|
125
|
+
widget.add_class("settings-value-cycle-unselected")
|
|
126
|
+
|
|
127
|
+
def action_move_up(self) -> None:
|
|
128
|
+
self.selected_index = (self.selected_index - 1) % len(self.settings)
|
|
129
|
+
self._update_display()
|
|
130
|
+
|
|
131
|
+
def action_move_down(self) -> None:
|
|
132
|
+
self.selected_index = (self.selected_index + 1) % len(self.settings)
|
|
133
|
+
self._update_display()
|
|
134
|
+
|
|
135
|
+
def action_toggle_setting(self) -> None:
|
|
136
|
+
setting = self.settings[self.selected_index]
|
|
137
|
+
key: str = setting["key"]
|
|
138
|
+
current: str = self._get_display_value(setting)
|
|
139
|
+
|
|
140
|
+
options: list[str] = setting["options"]
|
|
141
|
+
new_value = ""
|
|
142
|
+
try:
|
|
143
|
+
current_idx = options.index(current)
|
|
144
|
+
next_idx = (current_idx + 1) % len(options)
|
|
145
|
+
new_value = options[next_idx]
|
|
146
|
+
except (ValueError, IndexError):
|
|
147
|
+
new_value = options[0] if options else current
|
|
148
|
+
|
|
149
|
+
self.changes[key] = new_value
|
|
150
|
+
|
|
151
|
+
self.post_message(self.SettingChanged(key=key, value=new_value))
|
|
152
|
+
|
|
153
|
+
self._update_display()
|
|
154
|
+
|
|
155
|
+
def action_cycle(self) -> None:
|
|
156
|
+
self.action_toggle_setting()
|
|
157
|
+
|
|
158
|
+
def _convert_changes_for_save(self) -> dict[str, str | bool]:
|
|
159
|
+
result: dict[str, str | bool] = {}
|
|
160
|
+
for key, value in self.changes.items():
|
|
161
|
+
if value in {"On", "Off"}:
|
|
162
|
+
result[key] = value == "On"
|
|
163
|
+
else:
|
|
164
|
+
result[key] = value
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
def action_close(self) -> None:
|
|
168
|
+
self.post_message(self.ConfigClosed(changes=self._convert_changes_for_save()))
|
|
169
|
+
|
|
170
|
+
def on_blur(self, event: events.Blur) -> None:
|
|
171
|
+
self.call_after_refresh(self.focus)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from textual.reactive import reactive
|
|
7
|
+
|
|
8
|
+
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class TokenState:
|
|
13
|
+
max_tokens: int = 0
|
|
14
|
+
current_tokens: int = 0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ContextProgress(NoMarkupStatic):
|
|
18
|
+
tokens = reactive(TokenState())
|
|
19
|
+
|
|
20
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
21
|
+
super().__init__(**kwargs)
|
|
22
|
+
|
|
23
|
+
def watch_tokens(self, new_state: TokenState) -> None:
|
|
24
|
+
if new_state.max_tokens == 0:
|
|
25
|
+
self.update("")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
ratio = min(1, new_state.current_tokens / new_state.max_tokens)
|
|
29
|
+
text = f"{ratio:.0%} of {new_state.max_tokens // 1000}k tokens"
|
|
30
|
+
self.update(text)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import Horizontal
|
|
5
|
+
from textual.message import Message
|
|
6
|
+
from textual.widgets import Button, Static
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HistoryLoadMoreRequested(Message):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HistoryLoadMoreMessage(Static):
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
super().__init__()
|
|
16
|
+
self.add_class("history-load-more-message")
|
|
17
|
+
self._label_widget: Button | None = None
|
|
18
|
+
self._remaining: int | None = None
|
|
19
|
+
|
|
20
|
+
def compose(self) -> ComposeResult:
|
|
21
|
+
with Horizontal(classes="history-load-more-container"):
|
|
22
|
+
self._label_widget = Button(
|
|
23
|
+
self._label_text(), classes="history-load-more-button"
|
|
24
|
+
)
|
|
25
|
+
yield self._label_widget
|
|
26
|
+
|
|
27
|
+
def _label_text(self) -> str:
|
|
28
|
+
if self._remaining is None:
|
|
29
|
+
return "Load more messages"
|
|
30
|
+
return f"Load more messages ({self._remaining})"
|
|
31
|
+
|
|
32
|
+
def set_enabled(self, enabled: bool) -> None:
|
|
33
|
+
if self._label_widget:
|
|
34
|
+
self._label_widget.disabled = not enabled
|
|
35
|
+
|
|
36
|
+
def set_remaining(self, remaining: int | None) -> None:
|
|
37
|
+
self._remaining = remaining
|
|
38
|
+
if self._label_widget:
|
|
39
|
+
self._label_widget.label = self._label_text()
|
|
40
|
+
|
|
41
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
42
|
+
event.stop()
|
|
43
|
+
self.post_message(HistoryLoadMoreRequested())
|