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,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
# for more details on braille characters encoding, see: https://en.wikipedia.org/wiki/Braille_Patterns
|
|
7
|
+
|
|
8
|
+
_BRAILLE_DOT_COUNT = 8
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _braille_dot_index(x: int, y: int) -> int:
|
|
12
|
+
"""returns the number associated with a dot in a braille character
|
|
13
|
+
x ∈ {0, 1}, y ∈ {0, 1, 2, 3}
|
|
14
|
+
-x->
|
|
15
|
+
| 1 4
|
|
16
|
+
y 2 5
|
|
17
|
+
| 3 6
|
|
18
|
+
V 7 8
|
|
19
|
+
"""
|
|
20
|
+
if y < 3: # noqa: PLR2004
|
|
21
|
+
return y + 1 + 3 * x
|
|
22
|
+
return 7 + x
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _braille_char_from_dot_indices(indices: list[int]) -> str:
|
|
26
|
+
if any(n < 1 or n > _BRAILLE_DOT_COUNT for n in indices):
|
|
27
|
+
raise ValueError(f"Invalid braille dot indices: {indices}")
|
|
28
|
+
return chr(0x2800 + sum(2 ** (d - 1) for d in indices)) if indices else " "
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def render_braille(dot_coords: Iterable[complex], width: int, height: int) -> str:
|
|
32
|
+
"""this function receives a list of dot coordinantes, a width and a height,
|
|
33
|
+
and returns a string representing these dots with braille characters.
|
|
34
|
+
|
|
35
|
+
Origin is (0,0) and is located at the top left:
|
|
36
|
+
0----x---->
|
|
37
|
+
|
|
|
38
|
+
y
|
|
39
|
+
|
|
|
40
|
+
V
|
|
41
|
+
"""
|
|
42
|
+
dots_matrix: list[list[list[int]]] = [
|
|
43
|
+
[[] for _ in range(math.ceil(width / 2))] for _ in range(math.ceil(height / 4))
|
|
44
|
+
] # the list of dots for each character in the final str
|
|
45
|
+
|
|
46
|
+
for coord in dot_coords:
|
|
47
|
+
x = int(coord.real // 2)
|
|
48
|
+
y = int(coord.imag // 4)
|
|
49
|
+
sub_x = int(coord.real) % 2
|
|
50
|
+
sub_y = int(coord.imag) % 4
|
|
51
|
+
dots_matrix[y][x].append(_braille_dot_index(sub_x, sub_y))
|
|
52
|
+
|
|
53
|
+
braille_chars = [
|
|
54
|
+
[_braille_char_from_dot_indices(char_dots) for char_dots in row]
|
|
55
|
+
for row in dots_matrix
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
return "\n".join("".join(row) for row in braille_chars)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from vibe.cli.textual_ui.widgets.chat_input.body import ChatInputBody
|
|
4
|
+
from vibe.cli.textual_ui.widgets.chat_input.container import ChatInputContainer
|
|
5
|
+
from vibe.cli.textual_ui.widgets.chat_input.text_area import ChatTextArea
|
|
6
|
+
|
|
7
|
+
__all__ = ["ChatInputBody", "ChatInputContainer", "ChatTextArea"]
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.containers import Horizontal
|
|
9
|
+
from textual.message import Message
|
|
10
|
+
from textual.widget import Widget
|
|
11
|
+
|
|
12
|
+
from vibe.cli.history_manager import HistoryManager
|
|
13
|
+
from vibe.cli.textual_ui.widgets.chat_input.text_area import ChatTextArea, InputMode
|
|
14
|
+
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ChatInputBody(Widget):
|
|
18
|
+
class Submitted(Message):
|
|
19
|
+
def __init__(self, value: str) -> None:
|
|
20
|
+
self.value = value
|
|
21
|
+
super().__init__()
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
history_file: Path | None = None,
|
|
26
|
+
nuage_enabled: bool = False,
|
|
27
|
+
**kwargs: Any,
|
|
28
|
+
) -> None:
|
|
29
|
+
super().__init__(**kwargs)
|
|
30
|
+
self.input_widget: ChatTextArea | None = None
|
|
31
|
+
self.prompt_widget: NoMarkupStatic | None = None
|
|
32
|
+
self._nuage_enabled = nuage_enabled
|
|
33
|
+
|
|
34
|
+
if history_file:
|
|
35
|
+
self.history = HistoryManager(history_file)
|
|
36
|
+
else:
|
|
37
|
+
self.history = None
|
|
38
|
+
|
|
39
|
+
self._completion_reset: Callable[[], None] | None = None
|
|
40
|
+
|
|
41
|
+
def compose(self) -> ComposeResult:
|
|
42
|
+
with Horizontal():
|
|
43
|
+
self.prompt_widget = NoMarkupStatic(">", id="prompt")
|
|
44
|
+
yield self.prompt_widget
|
|
45
|
+
|
|
46
|
+
self.input_widget = ChatTextArea(
|
|
47
|
+
id="input", nuage_enabled=self._nuage_enabled
|
|
48
|
+
)
|
|
49
|
+
yield self.input_widget
|
|
50
|
+
|
|
51
|
+
def on_mount(self) -> None:
|
|
52
|
+
if self.input_widget:
|
|
53
|
+
self.input_widget.focus()
|
|
54
|
+
|
|
55
|
+
def _parse_mode_and_text(self, text: str) -> tuple[InputMode, str]:
|
|
56
|
+
if text.startswith("!"):
|
|
57
|
+
return "!", text[1:]
|
|
58
|
+
elif text.startswith("/"):
|
|
59
|
+
return "/", text[1:]
|
|
60
|
+
elif text.startswith("&") and self._nuage_enabled:
|
|
61
|
+
return "&", text[1:]
|
|
62
|
+
else:
|
|
63
|
+
return ">", text
|
|
64
|
+
|
|
65
|
+
def _update_prompt(self) -> None:
|
|
66
|
+
if not self.input_widget or not self.prompt_widget:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
self.prompt_widget.update(self.input_widget.input_mode)
|
|
70
|
+
|
|
71
|
+
def on_chat_text_area_mode_changed(self, event: ChatTextArea.ModeChanged) -> None:
|
|
72
|
+
if self.prompt_widget:
|
|
73
|
+
self.prompt_widget.update(event.mode)
|
|
74
|
+
|
|
75
|
+
def _load_history_entry(self, text: str, cursor_col: int | None = None) -> None:
|
|
76
|
+
if not self.input_widget:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
mode, display_text = self._parse_mode_and_text(text)
|
|
80
|
+
|
|
81
|
+
self.input_widget._navigating_history = True
|
|
82
|
+
self.input_widget.set_mode(mode)
|
|
83
|
+
self.input_widget.load_text(display_text)
|
|
84
|
+
|
|
85
|
+
first_line = display_text.split("\n")[0]
|
|
86
|
+
col = cursor_col if cursor_col is not None else len(first_line)
|
|
87
|
+
cursor_pos = (0, col)
|
|
88
|
+
|
|
89
|
+
self.input_widget.move_cursor(cursor_pos)
|
|
90
|
+
self.input_widget._last_cursor_col = col
|
|
91
|
+
self.input_widget._cursor_pos_after_load = cursor_pos
|
|
92
|
+
self.input_widget._cursor_moved_since_load = False
|
|
93
|
+
|
|
94
|
+
self._update_prompt()
|
|
95
|
+
self._notify_completion_reset()
|
|
96
|
+
|
|
97
|
+
def on_chat_text_area_history_previous(
|
|
98
|
+
self, event: ChatTextArea.HistoryPrevious
|
|
99
|
+
) -> None:
|
|
100
|
+
if not self.history or not self.input_widget:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
if self.history._current_index == -1:
|
|
104
|
+
self.input_widget._original_text = self.input_widget.text
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
self.history._current_index != -1
|
|
108
|
+
and self.input_widget._last_used_prefix is not None
|
|
109
|
+
and self.input_widget._last_used_prefix != event.prefix
|
|
110
|
+
):
|
|
111
|
+
self.history.reset_navigation()
|
|
112
|
+
|
|
113
|
+
self.input_widget._last_used_prefix = event.prefix
|
|
114
|
+
previous = self.history.get_previous(
|
|
115
|
+
self.input_widget._original_text, prefix=event.prefix
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if previous is not None:
|
|
119
|
+
self._load_history_entry(previous)
|
|
120
|
+
|
|
121
|
+
def on_chat_text_area_history_next(self, event: ChatTextArea.HistoryNext) -> None:
|
|
122
|
+
if not self.history or not self.input_widget:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
if self.history._current_index == -1:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
self.input_widget._last_used_prefix is not None
|
|
130
|
+
and self.input_widget._last_used_prefix != event.prefix
|
|
131
|
+
):
|
|
132
|
+
self.history.reset_navigation()
|
|
133
|
+
|
|
134
|
+
self.input_widget._last_used_prefix = event.prefix
|
|
135
|
+
|
|
136
|
+
has_next = any(
|
|
137
|
+
self.history._entries[i].startswith(event.prefix)
|
|
138
|
+
for i in range(self.history._current_index + 1, len(self.history._entries))
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
original_matches = self.input_widget._original_text.startswith(event.prefix)
|
|
142
|
+
|
|
143
|
+
if has_next or original_matches:
|
|
144
|
+
next_entry = self.history.get_next(prefix=event.prefix)
|
|
145
|
+
if next_entry is not None:
|
|
146
|
+
cursor_col = (
|
|
147
|
+
len(event.prefix) if self.history._current_index == -1 else None
|
|
148
|
+
)
|
|
149
|
+
self._load_history_entry(next_entry, cursor_col=cursor_col)
|
|
150
|
+
|
|
151
|
+
def on_chat_text_area_history_reset(self, event: ChatTextArea.HistoryReset) -> None:
|
|
152
|
+
if self.history:
|
|
153
|
+
self.history.reset_navigation()
|
|
154
|
+
if self.input_widget:
|
|
155
|
+
self.input_widget._original_text = ""
|
|
156
|
+
self.input_widget._cursor_pos_after_load = None
|
|
157
|
+
self.input_widget._cursor_moved_since_load = False
|
|
158
|
+
|
|
159
|
+
def on_chat_text_area_submitted(self, event: ChatTextArea.Submitted) -> None:
|
|
160
|
+
event.stop()
|
|
161
|
+
|
|
162
|
+
if not self.input_widget:
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
value = event.value.strip()
|
|
166
|
+
if value:
|
|
167
|
+
if self.history:
|
|
168
|
+
self.history.add(value)
|
|
169
|
+
self.history.reset_navigation()
|
|
170
|
+
|
|
171
|
+
self.input_widget.clear_text()
|
|
172
|
+
self._update_prompt()
|
|
173
|
+
|
|
174
|
+
self._notify_completion_reset()
|
|
175
|
+
|
|
176
|
+
self.post_message(self.Submitted(value))
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def value(self) -> str:
|
|
180
|
+
if not self.input_widget:
|
|
181
|
+
return ""
|
|
182
|
+
return self.input_widget.get_full_text()
|
|
183
|
+
|
|
184
|
+
@value.setter
|
|
185
|
+
def value(self, text: str) -> None:
|
|
186
|
+
if self.input_widget:
|
|
187
|
+
mode, display_text = self._parse_mode_and_text(text)
|
|
188
|
+
self.input_widget.set_mode(mode)
|
|
189
|
+
self.input_widget.load_text(display_text)
|
|
190
|
+
self._update_prompt()
|
|
191
|
+
|
|
192
|
+
def focus_input(self) -> None:
|
|
193
|
+
if self.input_widget:
|
|
194
|
+
self.input_widget.focus()
|
|
195
|
+
|
|
196
|
+
def set_completion_reset_callback(
|
|
197
|
+
self, callback: Callable[[], None] | None
|
|
198
|
+
) -> None:
|
|
199
|
+
self._completion_reset = callback
|
|
200
|
+
|
|
201
|
+
def _notify_completion_reset(self) -> None:
|
|
202
|
+
if self._completion_reset:
|
|
203
|
+
self._completion_reset()
|
|
204
|
+
|
|
205
|
+
def replace_input(self, text: str, cursor_offset: int | None = None) -> None:
|
|
206
|
+
if not self.input_widget:
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
self.input_widget.load_text(text)
|
|
210
|
+
self.input_widget.reset_history_state()
|
|
211
|
+
self._update_prompt()
|
|
212
|
+
|
|
213
|
+
if cursor_offset is not None:
|
|
214
|
+
self.input_widget.set_cursor_offset(max(0, min(cursor_offset, len(text))))
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from textual import events
|
|
7
|
+
|
|
8
|
+
from vibe.cli.autocompletion.base import CompletionResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CompletionController(Protocol):
|
|
12
|
+
def can_handle(self, text: str, cursor_index: int) -> bool: ...
|
|
13
|
+
|
|
14
|
+
def on_text_changed(self, text: str, cursor_index: int) -> None: ...
|
|
15
|
+
|
|
16
|
+
def on_key(
|
|
17
|
+
self, event: events.Key, text: str, cursor_index: int
|
|
18
|
+
) -> CompletionResult: ...
|
|
19
|
+
|
|
20
|
+
def reset(self) -> None: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MultiCompletionManager:
|
|
24
|
+
def __init__(self, controllers: Sequence[CompletionController]) -> None:
|
|
25
|
+
self._controllers = list(controllers)
|
|
26
|
+
self._active: CompletionController | None = None
|
|
27
|
+
|
|
28
|
+
def on_text_changed(self, text: str, cursor_index: int) -> None:
|
|
29
|
+
candidate = None
|
|
30
|
+
for controller in self._controllers:
|
|
31
|
+
if controller.can_handle(text, cursor_index):
|
|
32
|
+
candidate = controller
|
|
33
|
+
break
|
|
34
|
+
|
|
35
|
+
if candidate is None:
|
|
36
|
+
if self._active is not None:
|
|
37
|
+
self._active.reset()
|
|
38
|
+
self._active = None
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
if candidate is not self._active:
|
|
42
|
+
if self._active is not None:
|
|
43
|
+
self._active.reset()
|
|
44
|
+
self._active = candidate
|
|
45
|
+
|
|
46
|
+
candidate.on_text_changed(text, cursor_index)
|
|
47
|
+
|
|
48
|
+
def on_key(
|
|
49
|
+
self, event: events.Key, text: str, cursor_index: int
|
|
50
|
+
) -> CompletionResult:
|
|
51
|
+
if self._active is None:
|
|
52
|
+
return CompletionResult.IGNORED
|
|
53
|
+
return self._active.on_key(event, text, cursor_index)
|
|
54
|
+
|
|
55
|
+
def reset(self) -> None:
|
|
56
|
+
if self._active is not None:
|
|
57
|
+
self._active.reset()
|
|
58
|
+
self._active = None
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
from textual.widgets import Static
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CompletionPopup(Static):
|
|
10
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
11
|
+
super().__init__("", id="completion-popup", **kwargs)
|
|
12
|
+
self.styles.display = "none"
|
|
13
|
+
self.can_focus = False
|
|
14
|
+
|
|
15
|
+
def update_suggestions(
|
|
16
|
+
self, suggestions: list[tuple[str, str]], selected: int
|
|
17
|
+
) -> None:
|
|
18
|
+
if not suggestions:
|
|
19
|
+
self.hide()
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
text = Text()
|
|
23
|
+
for idx, (label, description) in enumerate(suggestions):
|
|
24
|
+
if idx:
|
|
25
|
+
text.append("\n")
|
|
26
|
+
|
|
27
|
+
label_style = "bold reverse" if idx == selected else "bold"
|
|
28
|
+
description_style = "italic" if idx == selected else "dim"
|
|
29
|
+
|
|
30
|
+
text.append(label, style=label_style)
|
|
31
|
+
if description:
|
|
32
|
+
text.append(" ")
|
|
33
|
+
text.append(description, style=description_style)
|
|
34
|
+
|
|
35
|
+
self.update(text)
|
|
36
|
+
self.show()
|
|
37
|
+
|
|
38
|
+
def hide(self) -> None:
|
|
39
|
+
self.update("")
|
|
40
|
+
self.styles.display = "none"
|
|
41
|
+
|
|
42
|
+
def show(self) -> None:
|
|
43
|
+
self.styles.display = "block"
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.containers import Vertical
|
|
9
|
+
from textual.message import Message
|
|
10
|
+
|
|
11
|
+
from vibe.cli.autocompletion.path_completion import PathCompletionController
|
|
12
|
+
from vibe.cli.autocompletion.slash_command import SlashCommandController
|
|
13
|
+
from vibe.cli.commands import CommandRegistry
|
|
14
|
+
from vibe.cli.textual_ui.widgets.chat_input.body import ChatInputBody
|
|
15
|
+
from vibe.cli.textual_ui.widgets.chat_input.completion_manager import (
|
|
16
|
+
MultiCompletionManager,
|
|
17
|
+
)
|
|
18
|
+
from vibe.cli.textual_ui.widgets.chat_input.completion_popup import CompletionPopup
|
|
19
|
+
from vibe.cli.textual_ui.widgets.chat_input.text_area import ChatTextArea
|
|
20
|
+
from vibe.core.agents import AgentSafety
|
|
21
|
+
from vibe.core.autocompletion.completers import CommandCompleter, PathCompleter
|
|
22
|
+
|
|
23
|
+
SAFETY_BORDER_CLASSES: dict[AgentSafety, str] = {
|
|
24
|
+
AgentSafety.SAFE: "border-safe",
|
|
25
|
+
AgentSafety.DESTRUCTIVE: "border-warning",
|
|
26
|
+
AgentSafety.YOLO: "border-error",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ChatInputContainer(Vertical):
|
|
31
|
+
ID_INPUT_BOX = "input-box"
|
|
32
|
+
|
|
33
|
+
class Submitted(Message):
|
|
34
|
+
def __init__(self, value: str) -> None:
|
|
35
|
+
self.value = value
|
|
36
|
+
super().__init__()
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
history_file: Path | None = None,
|
|
41
|
+
command_registry: CommandRegistry | None = None,
|
|
42
|
+
safety: AgentSafety = AgentSafety.NEUTRAL,
|
|
43
|
+
agent_name: str = "",
|
|
44
|
+
skill_entries_getter: Callable[[], list[tuple[str, str]]] | None = None,
|
|
45
|
+
nuage_enabled: bool = False,
|
|
46
|
+
**kwargs: Any,
|
|
47
|
+
) -> None:
|
|
48
|
+
super().__init__(**kwargs)
|
|
49
|
+
self._history_file = history_file
|
|
50
|
+
self._command_registry = command_registry or CommandRegistry()
|
|
51
|
+
self._safety = safety
|
|
52
|
+
self._agent_name = agent_name
|
|
53
|
+
self._skill_entries_getter = skill_entries_getter
|
|
54
|
+
self._nuage_enabled = nuage_enabled
|
|
55
|
+
|
|
56
|
+
self._completion_manager = MultiCompletionManager([
|
|
57
|
+
SlashCommandController(CommandCompleter(self._get_slash_entries), self),
|
|
58
|
+
PathCompletionController(PathCompleter(), self),
|
|
59
|
+
])
|
|
60
|
+
self._completion_popup: CompletionPopup | None = None
|
|
61
|
+
self._body: ChatInputBody | None = None
|
|
62
|
+
|
|
63
|
+
def _get_slash_entries(self) -> list[tuple[str, str]]:
|
|
64
|
+
entries = [
|
|
65
|
+
(alias, command.description)
|
|
66
|
+
for command in self._command_registry.commands.values()
|
|
67
|
+
for alias in sorted(command.aliases)
|
|
68
|
+
]
|
|
69
|
+
if self._skill_entries_getter:
|
|
70
|
+
entries.extend(self._skill_entries_getter())
|
|
71
|
+
return sorted(entries)
|
|
72
|
+
|
|
73
|
+
def compose(self) -> ComposeResult:
|
|
74
|
+
self._completion_popup = CompletionPopup()
|
|
75
|
+
yield self._completion_popup
|
|
76
|
+
|
|
77
|
+
border_class = SAFETY_BORDER_CLASSES.get(self._safety, "")
|
|
78
|
+
with Vertical(id=self.ID_INPUT_BOX, classes=border_class) as input_box:
|
|
79
|
+
input_box.border_title = self._agent_name
|
|
80
|
+
self._body = ChatInputBody(
|
|
81
|
+
history_file=self._history_file,
|
|
82
|
+
id="input-body",
|
|
83
|
+
nuage_enabled=self._nuage_enabled,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
yield self._body
|
|
87
|
+
|
|
88
|
+
def on_mount(self) -> None:
|
|
89
|
+
if not self._body:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
self._body.set_completion_reset_callback(self._completion_manager.reset)
|
|
93
|
+
if self._body.input_widget:
|
|
94
|
+
self._body.input_widget.set_completion_manager(self._completion_manager)
|
|
95
|
+
self._body.focus_input()
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def input_widget(self) -> ChatTextArea | None:
|
|
99
|
+
return self._body.input_widget if self._body else None
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def value(self) -> str:
|
|
103
|
+
if not self._body:
|
|
104
|
+
return ""
|
|
105
|
+
return self._body.value
|
|
106
|
+
|
|
107
|
+
@value.setter
|
|
108
|
+
def value(self, text: str) -> None:
|
|
109
|
+
if not self._body:
|
|
110
|
+
return
|
|
111
|
+
self._body.value = text
|
|
112
|
+
widget = self._body.input_widget
|
|
113
|
+
if widget:
|
|
114
|
+
self._completion_manager.on_text_changed(
|
|
115
|
+
widget.get_full_text(), widget._get_full_cursor_offset()
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def focus_input(self) -> None:
|
|
119
|
+
if self._body:
|
|
120
|
+
self._body.focus_input()
|
|
121
|
+
|
|
122
|
+
def render_completion_suggestions(
|
|
123
|
+
self, suggestions: list[tuple[str, str]], selected_index: int
|
|
124
|
+
) -> None:
|
|
125
|
+
if self._completion_popup:
|
|
126
|
+
self._completion_popup.update_suggestions(suggestions, selected_index)
|
|
127
|
+
|
|
128
|
+
def clear_completion_suggestions(self) -> None:
|
|
129
|
+
if self._completion_popup:
|
|
130
|
+
self._completion_popup.hide()
|
|
131
|
+
|
|
132
|
+
def _format_insertion(self, replacement: str, suffix: str) -> str:
|
|
133
|
+
"""Format the insertion text with appropriate spacing.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
replacement: The text to insert
|
|
137
|
+
suffix: The text that follows the insertion point
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The formatted insertion text with spacing if needed
|
|
141
|
+
"""
|
|
142
|
+
if replacement.startswith("@"):
|
|
143
|
+
if replacement.endswith("/"):
|
|
144
|
+
return replacement
|
|
145
|
+
# For @-prefixed completions, add space unless suffix starts with whitespace
|
|
146
|
+
return replacement + (" " if not suffix or not suffix[0].isspace() else "")
|
|
147
|
+
|
|
148
|
+
# For other completions, add space only if suffix exists and doesn't start with whitespace
|
|
149
|
+
return replacement + (" " if suffix and not suffix[0].isspace() else "")
|
|
150
|
+
|
|
151
|
+
def replace_completion_range(self, start: int, end: int, replacement: str) -> None:
|
|
152
|
+
widget = self.input_widget
|
|
153
|
+
if not widget or not self._body:
|
|
154
|
+
return
|
|
155
|
+
start, end, replacement = widget.adjust_from_full_text_coords(
|
|
156
|
+
start, end, replacement
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
text = widget.text
|
|
160
|
+
start = max(0, min(start, len(text)))
|
|
161
|
+
end = max(start, min(end, len(text)))
|
|
162
|
+
|
|
163
|
+
prefix = text[:start]
|
|
164
|
+
suffix = text[end:]
|
|
165
|
+
insertion = self._format_insertion(replacement, suffix)
|
|
166
|
+
new_text = f"{prefix}{insertion}{suffix}"
|
|
167
|
+
|
|
168
|
+
self._body.replace_input(new_text, cursor_offset=start + len(insertion))
|
|
169
|
+
|
|
170
|
+
def on_chat_input_body_submitted(self, event: ChatInputBody.Submitted) -> None:
|
|
171
|
+
event.stop()
|
|
172
|
+
self.post_message(self.Submitted(event.value))
|
|
173
|
+
|
|
174
|
+
def set_safety(self, safety: AgentSafety) -> None:
|
|
175
|
+
self._safety = safety
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
input_box = self.get_widget_by_id(self.ID_INPUT_BOX)
|
|
179
|
+
except Exception:
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
for border_class in SAFETY_BORDER_CLASSES.values():
|
|
183
|
+
input_box.remove_class(border_class)
|
|
184
|
+
|
|
185
|
+
if safety in SAFETY_BORDER_CLASSES:
|
|
186
|
+
input_box.add_class(SAFETY_BORDER_CLASSES[safety])
|
|
187
|
+
|
|
188
|
+
def set_agent_name(self, name: str) -> None:
|
|
189
|
+
self._agent_name = name
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
input_box = self.get_widget_by_id(self.ID_INPUT_BOX)
|
|
193
|
+
input_box.border_title = name
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|