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.
Files changed (170) hide show
  1. codemaster_cli-2.2.0.dist-info/METADATA +645 -0
  2. codemaster_cli-2.2.0.dist-info/RECORD +170 -0
  3. codemaster_cli-2.2.0.dist-info/WHEEL +4 -0
  4. codemaster_cli-2.2.0.dist-info/entry_points.txt +3 -0
  5. vibe/__init__.py +6 -0
  6. vibe/acp/__init__.py +0 -0
  7. vibe/acp/acp_agent_loop.py +746 -0
  8. vibe/acp/entrypoint.py +81 -0
  9. vibe/acp/tools/__init__.py +0 -0
  10. vibe/acp/tools/base.py +100 -0
  11. vibe/acp/tools/builtins/bash.py +134 -0
  12. vibe/acp/tools/builtins/read_file.py +54 -0
  13. vibe/acp/tools/builtins/search_replace.py +129 -0
  14. vibe/acp/tools/builtins/todo.py +65 -0
  15. vibe/acp/tools/builtins/write_file.py +98 -0
  16. vibe/acp/tools/session_update.py +118 -0
  17. vibe/acp/utils.py +213 -0
  18. vibe/cli/__init__.py +0 -0
  19. vibe/cli/autocompletion/__init__.py +0 -0
  20. vibe/cli/autocompletion/base.py +22 -0
  21. vibe/cli/autocompletion/path_completion.py +177 -0
  22. vibe/cli/autocompletion/slash_command.py +99 -0
  23. vibe/cli/cli.py +188 -0
  24. vibe/cli/clipboard.py +69 -0
  25. vibe/cli/commands.py +116 -0
  26. vibe/cli/entrypoint.py +163 -0
  27. vibe/cli/history_manager.py +91 -0
  28. vibe/cli/plan_offer/adapters/http_whoami_gateway.py +67 -0
  29. vibe/cli/plan_offer/decide_plan_offer.py +87 -0
  30. vibe/cli/plan_offer/ports/whoami_gateway.py +23 -0
  31. vibe/cli/terminal_setup.py +323 -0
  32. vibe/cli/textual_ui/__init__.py +0 -0
  33. vibe/cli/textual_ui/ansi_markdown.py +58 -0
  34. vibe/cli/textual_ui/app.py +1546 -0
  35. vibe/cli/textual_ui/app.tcss +1020 -0
  36. vibe/cli/textual_ui/external_editor.py +32 -0
  37. vibe/cli/textual_ui/handlers/__init__.py +5 -0
  38. vibe/cli/textual_ui/handlers/event_handler.py +147 -0
  39. vibe/cli/textual_ui/widgets/__init__.py +0 -0
  40. vibe/cli/textual_ui/widgets/approval_app.py +192 -0
  41. vibe/cli/textual_ui/widgets/banner/banner.py +85 -0
  42. vibe/cli/textual_ui/widgets/banner/petit_chat.py +195 -0
  43. vibe/cli/textual_ui/widgets/braille_renderer.py +58 -0
  44. vibe/cli/textual_ui/widgets/chat_input/__init__.py +7 -0
  45. vibe/cli/textual_ui/widgets/chat_input/body.py +214 -0
  46. vibe/cli/textual_ui/widgets/chat_input/completion_manager.py +58 -0
  47. vibe/cli/textual_ui/widgets/chat_input/completion_popup.py +43 -0
  48. vibe/cli/textual_ui/widgets/chat_input/container.py +195 -0
  49. vibe/cli/textual_ui/widgets/chat_input/text_area.py +365 -0
  50. vibe/cli/textual_ui/widgets/compact.py +41 -0
  51. vibe/cli/textual_ui/widgets/config_app.py +171 -0
  52. vibe/cli/textual_ui/widgets/context_progress.py +30 -0
  53. vibe/cli/textual_ui/widgets/load_more.py +43 -0
  54. vibe/cli/textual_ui/widgets/loading.py +201 -0
  55. vibe/cli/textual_ui/widgets/messages.py +277 -0
  56. vibe/cli/textual_ui/widgets/no_markup_static.py +11 -0
  57. vibe/cli/textual_ui/widgets/path_display.py +28 -0
  58. vibe/cli/textual_ui/widgets/proxy_setup_app.py +127 -0
  59. vibe/cli/textual_ui/widgets/question_app.py +496 -0
  60. vibe/cli/textual_ui/widgets/spinner.py +194 -0
  61. vibe/cli/textual_ui/widgets/status_message.py +76 -0
  62. vibe/cli/textual_ui/widgets/teleport_message.py +31 -0
  63. vibe/cli/textual_ui/widgets/tool_widgets.py +371 -0
  64. vibe/cli/textual_ui/widgets/tools.py +201 -0
  65. vibe/cli/textual_ui/windowing/__init__.py +29 -0
  66. vibe/cli/textual_ui/windowing/history.py +105 -0
  67. vibe/cli/textual_ui/windowing/history_windowing.py +71 -0
  68. vibe/cli/textual_ui/windowing/state.py +105 -0
  69. vibe/cli/update_notifier/__init__.py +47 -0
  70. vibe/cli/update_notifier/adapters/filesystem_update_cache_repository.py +59 -0
  71. vibe/cli/update_notifier/adapters/github_update_gateway.py +101 -0
  72. vibe/cli/update_notifier/adapters/pypi_update_gateway.py +107 -0
  73. vibe/cli/update_notifier/ports/update_cache_repository.py +16 -0
  74. vibe/cli/update_notifier/ports/update_gateway.py +53 -0
  75. vibe/cli/update_notifier/update.py +139 -0
  76. vibe/cli/update_notifier/whats_new.py +49 -0
  77. vibe/core/__init__.py +5 -0
  78. vibe/core/agent_loop.py +1075 -0
  79. vibe/core/agents/__init__.py +31 -0
  80. vibe/core/agents/manager.py +165 -0
  81. vibe/core/agents/models.py +122 -0
  82. vibe/core/auth/__init__.py +6 -0
  83. vibe/core/auth/crypto.py +137 -0
  84. vibe/core/auth/github.py +178 -0
  85. vibe/core/autocompletion/__init__.py +0 -0
  86. vibe/core/autocompletion/completers.py +257 -0
  87. vibe/core/autocompletion/file_indexer/__init__.py +10 -0
  88. vibe/core/autocompletion/file_indexer/ignore_rules.py +156 -0
  89. vibe/core/autocompletion/file_indexer/indexer.py +179 -0
  90. vibe/core/autocompletion/file_indexer/store.py +169 -0
  91. vibe/core/autocompletion/file_indexer/watcher.py +71 -0
  92. vibe/core/autocompletion/fuzzy.py +189 -0
  93. vibe/core/autocompletion/path_prompt.py +108 -0
  94. vibe/core/autocompletion/path_prompt_adapter.py +149 -0
  95. vibe/core/config.py +673 -0
  96. vibe/core/config_PATCH_INSTRUCTIONS.md +77 -0
  97. vibe/core/llm/__init__.py +0 -0
  98. vibe/core/llm/backend/anthropic.py +630 -0
  99. vibe/core/llm/backend/base.py +38 -0
  100. vibe/core/llm/backend/factory.py +7 -0
  101. vibe/core/llm/backend/generic.py +425 -0
  102. vibe/core/llm/backend/mistral.py +381 -0
  103. vibe/core/llm/backend/vertex.py +115 -0
  104. vibe/core/llm/exceptions.py +195 -0
  105. vibe/core/llm/format.py +184 -0
  106. vibe/core/llm/message_utils.py +24 -0
  107. vibe/core/llm/types.py +120 -0
  108. vibe/core/middleware.py +209 -0
  109. vibe/core/output_formatters.py +85 -0
  110. vibe/core/paths/__init__.py +0 -0
  111. vibe/core/paths/config_paths.py +68 -0
  112. vibe/core/paths/global_paths.py +40 -0
  113. vibe/core/programmatic.py +56 -0
  114. vibe/core/prompts/__init__.py +32 -0
  115. vibe/core/prompts/cli.md +111 -0
  116. vibe/core/prompts/compact.md +48 -0
  117. vibe/core/prompts/dangerous_directory.md +5 -0
  118. vibe/core/prompts/explore.md +50 -0
  119. vibe/core/prompts/project_context.md +8 -0
  120. vibe/core/prompts/tests.md +1 -0
  121. vibe/core/proxy_setup.py +65 -0
  122. vibe/core/session/session_loader.py +222 -0
  123. vibe/core/session/session_logger.py +318 -0
  124. vibe/core/session/session_migration.py +41 -0
  125. vibe/core/skills/__init__.py +7 -0
  126. vibe/core/skills/manager.py +132 -0
  127. vibe/core/skills/models.py +92 -0
  128. vibe/core/skills/parser.py +39 -0
  129. vibe/core/system_prompt.py +466 -0
  130. vibe/core/telemetry/__init__.py +0 -0
  131. vibe/core/telemetry/send.py +185 -0
  132. vibe/core/teleport/errors.py +9 -0
  133. vibe/core/teleport/git.py +196 -0
  134. vibe/core/teleport/nuage.py +180 -0
  135. vibe/core/teleport/teleport.py +208 -0
  136. vibe/core/teleport/types.py +54 -0
  137. vibe/core/tools/base.py +336 -0
  138. vibe/core/tools/builtins/ask_user_question.py +134 -0
  139. vibe/core/tools/builtins/bash.py +357 -0
  140. vibe/core/tools/builtins/grep.py +310 -0
  141. vibe/core/tools/builtins/prompts/__init__.py +0 -0
  142. vibe/core/tools/builtins/prompts/ask_user_question.md +84 -0
  143. vibe/core/tools/builtins/prompts/bash.md +73 -0
  144. vibe/core/tools/builtins/prompts/grep.md +4 -0
  145. vibe/core/tools/builtins/prompts/read_file.md +13 -0
  146. vibe/core/tools/builtins/prompts/search_replace.md +43 -0
  147. vibe/core/tools/builtins/prompts/task.md +24 -0
  148. vibe/core/tools/builtins/prompts/todo.md +199 -0
  149. vibe/core/tools/builtins/prompts/write_file.md +42 -0
  150. vibe/core/tools/builtins/read_file.py +222 -0
  151. vibe/core/tools/builtins/search_replace.py +456 -0
  152. vibe/core/tools/builtins/task.py +154 -0
  153. vibe/core/tools/builtins/todo.py +134 -0
  154. vibe/core/tools/builtins/write_file.py +160 -0
  155. vibe/core/tools/manager.py +341 -0
  156. vibe/core/tools/mcp.py +397 -0
  157. vibe/core/tools/ui.py +68 -0
  158. vibe/core/trusted_folders.py +86 -0
  159. vibe/core/types.py +405 -0
  160. vibe/core/utils.py +396 -0
  161. vibe/setup/onboarding/__init__.py +39 -0
  162. vibe/setup/onboarding/base.py +14 -0
  163. vibe/setup/onboarding/onboarding.tcss +134 -0
  164. vibe/setup/onboarding/screens/__init__.py +5 -0
  165. vibe/setup/onboarding/screens/api_key.py +200 -0
  166. vibe/setup/onboarding/screens/provider_selection.py +87 -0
  167. vibe/setup/onboarding/screens/welcome.py +136 -0
  168. vibe/setup/trusted_folders/trust_folder_dialog.py +180 -0
  169. vibe/setup/trusted_folders/trust_folder_dialog.tcss +83 -0
  170. 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