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,200 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import ClassVar
5
+
6
+ from dotenv import set_key
7
+ from textual.app import ComposeResult
8
+ from textual.binding import Binding, BindingType
9
+ from textual.containers import Center, Horizontal, Vertical
10
+ from textual.events import MouseUp
11
+ from textual.validation import Length
12
+ from textual.widgets import Input, Link, Static
13
+
14
+ from vibe.cli.clipboard import copy_selection_to_clipboard
15
+ from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
16
+ from vibe.core.config import Backend, VibeConfig
17
+ from vibe.core.paths.global_paths import GLOBAL_ENV_FILE
18
+ from vibe.core.telemetry.send import TelemetryClient
19
+ from vibe.setup.onboarding.base import OnboardingScreen
20
+
21
+ PROVIDER_HELP = {
22
+ "mistral": ("https://console.mistral.ai/codestral/cli", "Mistral AI Studio")
23
+ }
24
+ CONFIG_DOCS_URL = (
25
+ "https://github.com/marshal0004/CodeMaster#configuration"
26
+ )
27
+
28
+
29
+ def _save_api_key_to_env_file(env_key: str, api_key: str) -> None:
30
+ GLOBAL_ENV_FILE.path.parent.mkdir(parents=True, exist_ok=True)
31
+ set_key(GLOBAL_ENV_FILE.path, env_key, api_key)
32
+
33
+
34
+ class ApiKeyScreen(OnboardingScreen):
35
+ BINDINGS: ClassVar[list[BindingType]] = [
36
+ Binding("ctrl+c", "cancel", "Cancel", show=False),
37
+ Binding("escape", "cancel", "Cancel", show=False),
38
+ ]
39
+
40
+ NEXT_SCREEN = None
41
+
42
+ def __init__(self) -> None:
43
+ super().__init__()
44
+ import os
45
+ # Check if model was selected in provider screen
46
+ selected_model = os.environ.get("_CODEMASTER_SELECTED_MODEL")
47
+ if selected_model:
48
+ try:
49
+ config = VibeConfig.model_construct()
50
+ config.active_model = selected_model
51
+ active_model = config.get_active_model()
52
+ self.provider = config.get_provider_for_model(active_model)
53
+ except Exception:
54
+ config = VibeConfig.model_construct()
55
+ active_model = config.get_active_model()
56
+ self.provider = config.get_provider_for_model(active_model)
57
+ else:
58
+ config = VibeConfig.model_construct()
59
+ active_model = config.get_active_model()
60
+ self.provider = config.get_provider_for_model(active_model)
61
+ # Ollama and other local providers don't require an API key
62
+ self._skip_key = not bool(self.provider.api_key_env_var)
63
+
64
+
65
+
66
+ def _compose_provider_link(self, provider_name: str) -> ComposeResult:
67
+ if self.provider.name not in PROVIDER_HELP:
68
+ return
69
+
70
+ help_url, help_name = PROVIDER_HELP[self.provider.name]
71
+ yield NoMarkupStatic(f"Grab your {provider_name} API key from the {help_name}:")
72
+ yield Center(
73
+ Horizontal(
74
+ NoMarkupStatic("→ ", classes="link-chevron"),
75
+ Link(help_url, url=help_url),
76
+ classes="link-row",
77
+ )
78
+ )
79
+
80
+ def _compose_config_docs(self) -> ComposeResult:
81
+ yield Static("[dim]Learn more about codeMaster configuration:[/]")
82
+ yield Horizontal(
83
+ NoMarkupStatic("→ ", classes="link-chevron"),
84
+ Link(CONFIG_DOCS_URL, url=CONFIG_DOCS_URL),
85
+ classes="link-row",
86
+ )
87
+
88
+ def _compose_local_provider(self) -> ComposeResult:
89
+ """Compose UI for local providers (e.g. Ollama) that need no API key."""
90
+ provider_name = self.provider.name.capitalize()
91
+ with Vertical(id="api-key-outer"):
92
+ yield NoMarkupStatic("", classes="spacer")
93
+ yield Center(NoMarkupStatic("Local provider detected!", id="api-key-title"))
94
+ with Center():
95
+ with Vertical(id="api-key-content"):
96
+ yield NoMarkupStatic(
97
+ f"Provider '{provider_name}' requires no API key.",
98
+ id="paste-hint",
99
+ )
100
+ yield NoMarkupStatic(
101
+ "Make sure Ollama is running: ollama serve",
102
+ id="ollama-hint",
103
+ )
104
+ yield NoMarkupStatic(
105
+ "Press Enter to continue ↵", id="enter-continue"
106
+ )
107
+ yield NoMarkupStatic("", classes="spacer")
108
+ yield Vertical(
109
+ Vertical(*self._compose_config_docs(), id="config-docs-group"),
110
+ id="config-docs-section",
111
+ )
112
+
113
+ def compose(self) -> ComposeResult:
114
+ if self._skip_key:
115
+ yield from self._compose_local_provider()
116
+ return
117
+
118
+ provider_name = self.provider.name.capitalize()
119
+
120
+ self.input_widget = Input(
121
+ password=True,
122
+ id="key",
123
+ placeholder="Paste your API key here",
124
+ validators=[Length(minimum=1, failure_description="No API key provided.")],
125
+ )
126
+
127
+ with Vertical(id="api-key-outer"):
128
+ yield NoMarkupStatic("", classes="spacer")
129
+ yield Center(NoMarkupStatic("One last thing...", id="api-key-title"))
130
+ with Center():
131
+ with Vertical(id="api-key-content"):
132
+ yield from self._compose_provider_link(provider_name)
133
+ yield NoMarkupStatic(
134
+ "...and paste it below to finish the setup:", id="paste-hint"
135
+ )
136
+ yield Center(Horizontal(self.input_widget, id="input-box"))
137
+ yield NoMarkupStatic("", id="feedback")
138
+ yield NoMarkupStatic("", classes="spacer")
139
+ yield Vertical(
140
+ Vertical(*self._compose_config_docs(), id="config-docs-group"),
141
+ id="config-docs-section",
142
+ )
143
+
144
+ def on_mount(self) -> None:
145
+ if self._skip_key:
146
+ # Local provider (Ollama etc.) — no API key required
147
+ self.focus()
148
+ return
149
+ self.input_widget.focus()
150
+
151
+ def on_key(self, event: object) -> None:
152
+ """Handle Enter key for local provider confirmation."""
153
+ if self._skip_key:
154
+ from textual.events import Key
155
+ if isinstance(event, Key) and event.key == "enter":
156
+ self.app.exit("completed")
157
+
158
+ def on_input_changed(self, event: Input.Changed) -> None:
159
+ feedback = self.query_one("#feedback", NoMarkupStatic)
160
+ input_box = self.query_one("#input-box")
161
+
162
+ if event.validation_result is None:
163
+ return
164
+
165
+ input_box.remove_class("valid", "invalid")
166
+ feedback.remove_class("error", "success")
167
+
168
+ if event.validation_result.is_valid:
169
+ feedback.update("Press Enter to submit ↵")
170
+ feedback.add_class("success")
171
+ input_box.add_class("valid")
172
+ return
173
+
174
+ descriptions = event.validation_result.failure_descriptions
175
+ feedback.update(descriptions[0])
176
+ feedback.add_class("error")
177
+ input_box.add_class("invalid")
178
+
179
+ def on_input_submitted(self, event: Input.Submitted) -> None:
180
+ if event.validation_result and event.validation_result.is_valid:
181
+ self._save_and_finish(event.value)
182
+
183
+ def _save_and_finish(self, api_key: str) -> None:
184
+ env_key = self.provider.api_key_env_var
185
+ os.environ[env_key] = api_key
186
+ try:
187
+ _save_api_key_to_env_file(env_key, api_key)
188
+ except OSError as err:
189
+ self.app.exit(f"save_error:{err}")
190
+ return
191
+ if self.provider.backend == Backend.MISTRAL:
192
+ try:
193
+ telemetry = TelemetryClient(config_getter=VibeConfig)
194
+ telemetry.send_onboarding_api_key_added()
195
+ except Exception:
196
+ pass # Telemetry is fire-and-forget; don't fail onboarding
197
+ self.app.exit("completed")
198
+
199
+ def on_mouse_up(self, event: MouseUp) -> None:
200
+ copy_selection_to_clipboard(self.app)
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import ClassVar
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.binding import Binding, BindingType
7
+ from textual.containers import Center, Vertical
8
+ from textual.widgets import Button, Static
9
+
10
+ from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
11
+ from vibe.core.config import VibeConfig
12
+ from vibe.setup.onboarding.base import OnboardingScreen
13
+
14
+
15
+ class ProviderSelectionScreen(OnboardingScreen):
16
+ BINDINGS: ClassVar[list[BindingType]] = [
17
+ Binding("ctrl+c", "cancel", "Cancel", show=False),
18
+ Binding("escape", "cancel", "Cancel", show=False),
19
+ ]
20
+
21
+ NEXT_SCREEN = "api_key"
22
+
23
+ def __init__(self) -> None:
24
+ super().__init__()
25
+ self.selected_provider: str = "ollama"
26
+
27
+ def compose(self) -> ComposeResult:
28
+ with Vertical(id="provider-selection-outer"):
29
+ yield NoMarkupStatic("", classes="spacer")
30
+ yield Center(
31
+ NoMarkupStatic("Choose your AI provider", id="provider-title")
32
+ )
33
+ with Center():
34
+ with Vertical(id="provider-content"):
35
+ yield NoMarkupStatic(
36
+ "Select how you want to run codeMaster:",
37
+ id="provider-description",
38
+ )
39
+ yield NoMarkupStatic("", classes="mini-spacer")
40
+
41
+ # Ollama option
42
+ yield Button(
43
+ "🏠 Local (Ollama) - Free, No API Key",
44
+ id="btn-ollama",
45
+ variant="primary",
46
+ )
47
+ yield NoMarkupStatic(
48
+ " → Runs on your machine using Ollama",
49
+ classes="provider-hint",
50
+ )
51
+ yield NoMarkupStatic("", classes="mini-spacer")
52
+
53
+ # Mistral option
54
+ yield Button(
55
+ "☁️ Cloud (Mistral) - Requires API Key",
56
+ id="btn-mistral",
57
+ variant="default",
58
+ )
59
+ yield NoMarkupStatic(
60
+ " → Fast cloud-based AI from Mistral",
61
+ classes="provider-hint",
62
+ )
63
+ yield NoMarkupStatic("", classes="spacer")
64
+
65
+ def on_mount(self) -> None:
66
+ self.query_one("#btn-ollama", Button).focus()
67
+
68
+ def on_button_pressed(self, event: Button.Pressed) -> None:
69
+ if event.button.id == "btn-ollama":
70
+ self.selected_provider = "ollama"
71
+ self._configure_and_next("qwen2.5-coder") # ✅ Supports tools!
72
+ elif event.button.id == "btn-mistral":
73
+ self.selected_provider = "mistral"
74
+ self._configure_and_next("devstral-2")
75
+
76
+ def _configure_and_next(self, model_alias: str) -> None:
77
+ """Save the selected model and proceed to API key screen."""
78
+ try:
79
+ VibeConfig.save_updates({"active_model": model_alias})
80
+ except Exception:
81
+ pass # Config will be created during onboarding completion
82
+
83
+ # Store selection for API key screen
84
+ import os
85
+ os.environ["_CODEMASTER_SELECTED_MODEL"] = model_alias
86
+
87
+ self.action_next()
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import ClassVar
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.binding import Binding, BindingType
7
+ from textual.containers import Center, Vertical
8
+ from textual.timer import Timer
9
+ from textual.widgets import Static
10
+
11
+ from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
12
+ from vibe.setup.onboarding.base import OnboardingScreen
13
+
14
+ WELCOME_PREFIX = "Welcome to "
15
+ WELCOME_HIGHLIGHT = "codeMaster"
16
+ WELCOME_SUFFIX = " - Let's get you started!"
17
+ WELCOME_TEXT = WELCOME_PREFIX + WELCOME_HIGHLIGHT + WELCOME_SUFFIX
18
+
19
+ HIGHLIGHT_START = len(WELCOME_PREFIX)
20
+ HIGHLIGHT_END = HIGHLIGHT_START + len(WELCOME_HIGHLIGHT)
21
+
22
+ BUTTON_TEXT = "Press Enter ↵"
23
+
24
+ GRADIENT_COLORS = [
25
+ "#ff6b00",
26
+ "#ff7b00",
27
+ "#ff8c00",
28
+ "#ff9d00",
29
+ "#ffae00",
30
+ "#ffbf00",
31
+ "#ffae00",
32
+ "#ff9d00",
33
+ "#ff8c00",
34
+ "#ff7b00",
35
+ ]
36
+
37
+
38
+ def _apply_gradient(text: str, offset: int) -> str:
39
+ result = []
40
+ for i, char in enumerate(text):
41
+ color = GRADIENT_COLORS[(i + offset) % len(GRADIENT_COLORS)]
42
+ result.append(f"[bold {color}]{char}[/]")
43
+ return "".join(result)
44
+
45
+
46
+ class WelcomeScreen(OnboardingScreen):
47
+ BINDINGS: ClassVar[list[BindingType]] = [
48
+ Binding("enter", "next", "Next", show=False, priority=True),
49
+ Binding("ctrl+c", "cancel", "Cancel", show=False),
50
+ Binding("escape", "cancel", "Cancel", show=False),
51
+ ]
52
+
53
+ NEXT_SCREEN = "provider_selection"
54
+
55
+ def __init__(self) -> None:
56
+ super().__init__()
57
+ self._char_index = 0
58
+ self._gradient_offset = 0
59
+ self._typing_done = False
60
+ self._paused = False
61
+ self._typing_timer: Timer | None = None
62
+ self._button_char_index = 0
63
+ self._button_typing_timer: Timer | None = None
64
+ self._welcome_text: Static
65
+ self._enter_hint: Static
66
+
67
+ def compose(self) -> ComposeResult:
68
+ with Vertical(id="welcome-container"):
69
+ with Center():
70
+ yield Static("", id="welcome-text")
71
+ with Center():
72
+ yield NoMarkupStatic("", id="enter-hint", classes="hidden")
73
+
74
+ def on_mount(self) -> None:
75
+ self._welcome_text = self.query_one("#welcome-text", Static)
76
+ self._enter_hint = self.query_one("#enter-hint", Static)
77
+ self._typing_timer = self.set_interval(0.04, self._type_next_char)
78
+ self.focus()
79
+
80
+ def _render_text(self, length: int) -> str:
81
+ text = WELCOME_TEXT[:length]
82
+
83
+ if length <= HIGHLIGHT_START:
84
+ return text
85
+
86
+ prefix = text[:HIGHLIGHT_START]
87
+ highlight_len = min(length, HIGHLIGHT_END) - HIGHLIGHT_START
88
+ highlight = _apply_gradient(
89
+ WELCOME_HIGHLIGHT[:highlight_len], self._gradient_offset
90
+ )
91
+
92
+ if length > HIGHLIGHT_END:
93
+ suffix = text[HIGHLIGHT_END:]
94
+ return prefix + highlight + suffix
95
+ return prefix + highlight
96
+
97
+ def _type_next_char(self) -> None:
98
+ if self._char_index >= len(WELCOME_TEXT):
99
+ if not self._typing_done:
100
+ self._typing_done = True
101
+ self.set_timer(0.5, self._show_button)
102
+ return
103
+
104
+ if self._char_index == HIGHLIGHT_END and not self._paused:
105
+ self._paused = True
106
+ if self._typing_timer:
107
+ self._typing_timer.stop()
108
+ self.set_interval(0.08, self._animate_gradient)
109
+ self.set_timer(1.4, self._resume_typing)
110
+ return
111
+
112
+ self._char_index += 1
113
+ self._welcome_text.update(self._render_text(self._char_index))
114
+
115
+ def _resume_typing(self) -> None:
116
+ self._typing_timer = self.set_interval(0.03, self._type_next_char)
117
+
118
+ def _show_button(self) -> None:
119
+ self._enter_hint.remove_class("hidden")
120
+ self._button_typing_timer = self.set_interval(0.03, self._type_button_char)
121
+
122
+ def _type_button_char(self) -> None:
123
+ if self._button_char_index >= len(BUTTON_TEXT):
124
+ if self._button_typing_timer:
125
+ self._button_typing_timer.stop()
126
+ return
127
+ self._button_char_index += 1
128
+ self._enter_hint.update(BUTTON_TEXT[: self._button_char_index])
129
+
130
+ def _animate_gradient(self) -> None:
131
+ self._gradient_offset = (self._gradient_offset + 1) % len(GRADIENT_COLORS)
132
+ self._welcome_text.update(self._render_text(self._char_index))
133
+
134
+ def action_next(self) -> None:
135
+ if self._typing_done:
136
+ super().action_next()
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any, ClassVar
5
+
6
+ from textual import events
7
+ from textual.app import App, ComposeResult
8
+ from textual.binding import Binding, BindingType
9
+ from textual.containers import CenterMiddle, Horizontal
10
+ from textual.message import Message
11
+ from textual.widgets import Static
12
+
13
+ from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
14
+ from vibe.core.paths.global_paths import TRUSTED_FOLDERS_FILE
15
+
16
+
17
+ class TrustDialogQuitException(Exception):
18
+ pass
19
+
20
+
21
+ class TrustFolderDialog(CenterMiddle):
22
+ can_focus = True
23
+ can_focus_children = True
24
+
25
+ BINDINGS: ClassVar[list[BindingType]] = [
26
+ Binding("left", "move_left", "Left", show=False),
27
+ Binding("right", "move_right", "Right", show=False),
28
+ Binding("enter", "select", "Select", show=False),
29
+ Binding("1", "select_1", "Yes", show=False),
30
+ Binding("y", "select_1", "Yes", show=False),
31
+ Binding("2", "select_2", "No", show=False),
32
+ Binding("n", "select_2", "No", show=False),
33
+ ]
34
+
35
+ class Trusted(Message):
36
+ pass
37
+
38
+ class Untrusted(Message):
39
+ pass
40
+
41
+ def __init__(self, folder_path: Path, **kwargs: Any) -> None:
42
+ super().__init__(**kwargs)
43
+ self.folder_path = folder_path
44
+ self.selected_option = 0
45
+ self.option_widgets: list[Static] = []
46
+
47
+ def compose(self) -> ComposeResult:
48
+ with CenterMiddle(id="trust-dialog"):
49
+ yield NoMarkupStatic("⚠ Trust this folder?", id="trust-dialog-title")
50
+ yield NoMarkupStatic(
51
+ str(self.folder_path),
52
+ id="trust-dialog-path",
53
+ classes="trust-dialog-path",
54
+ )
55
+ yield NoMarkupStatic(
56
+ "Files that can modify your codeMaster setup were found here. Do you trust this folder?",
57
+ id="trust-dialog-message",
58
+ classes="trust-dialog-message",
59
+ )
60
+
61
+ with Horizontal(id="trust-options-container"):
62
+ options = ["Yes", "No"]
63
+ for idx, text in enumerate(options):
64
+ widget = NoMarkupStatic(
65
+ f" {idx + 1}. {text}", classes="trust-option"
66
+ )
67
+ self.option_widgets.append(widget)
68
+ yield widget
69
+
70
+ yield NoMarkupStatic(
71
+ "← → navigate Enter select", classes="trust-dialog-help"
72
+ )
73
+
74
+ yield NoMarkupStatic(
75
+ f"Setting will be saved in: {TRUSTED_FOLDERS_FILE.path}",
76
+ id="trust-dialog-save-info",
77
+ classes="trust-dialog-save-info",
78
+ )
79
+
80
+ async def on_mount(self) -> None:
81
+ self.selected_option = 1 # Default to "No"
82
+ self._update_options()
83
+ self.focus()
84
+
85
+ def _update_options(self) -> None:
86
+ options = ["Yes", "No"]
87
+
88
+ if len(self.option_widgets) != len(options):
89
+ return
90
+
91
+ for idx, (text, widget) in enumerate(
92
+ zip(options, self.option_widgets, strict=True)
93
+ ):
94
+ is_selected = idx == self.selected_option
95
+
96
+ cursor = "› " if is_selected else " "
97
+ option_text = f"{cursor}{text}"
98
+
99
+ widget.update(option_text)
100
+
101
+ widget.remove_class("trust-cursor-selected")
102
+ widget.remove_class("trust-option-selected")
103
+
104
+ if is_selected:
105
+ widget.add_class("trust-cursor-selected")
106
+ else:
107
+ widget.add_class("trust-option-selected")
108
+
109
+ def action_move_left(self) -> None:
110
+ self.selected_option = (self.selected_option - 1) % 2
111
+ self._update_options()
112
+
113
+ def action_move_right(self) -> None:
114
+ self.selected_option = (self.selected_option + 1) % 2
115
+ self._update_options()
116
+
117
+ def action_select(self) -> None:
118
+ self._handle_selection(self.selected_option)
119
+
120
+ def action_select_1(self) -> None:
121
+ self.selected_option = 0
122
+ self._handle_selection(0)
123
+
124
+ def action_select_2(self) -> None:
125
+ self.selected_option = 1
126
+ self._handle_selection(1)
127
+
128
+ def _handle_selection(self, option: int) -> None:
129
+ match option:
130
+ case 0:
131
+ self.post_message(self.Trusted())
132
+ case 1:
133
+ self.post_message(self.Untrusted())
134
+
135
+ def on_blur(self, event: events.Blur) -> None:
136
+ self.call_after_refresh(self.focus)
137
+
138
+
139
+ class TrustFolderApp(App):
140
+ CSS_PATH = "trust_folder_dialog.tcss"
141
+
142
+ BINDINGS: ClassVar[list[BindingType]] = [
143
+ Binding("ctrl+q", "quit_without_saving", "Quit", show=False, priority=True),
144
+ Binding("ctrl+c", "quit_without_saving", "Quit", show=False, priority=True),
145
+ ]
146
+
147
+ def __init__(self, folder_path: Path, **kwargs: Any) -> None:
148
+ super().__init__(**kwargs)
149
+ self.folder_path = folder_path
150
+ self._result: bool | None = None
151
+ self._quit_without_saving = False
152
+
153
+ def on_mount(self) -> None:
154
+ self.theme = "textual-ansi"
155
+
156
+ def compose(self) -> ComposeResult:
157
+ yield TrustFolderDialog(self.folder_path)
158
+
159
+ def action_quit_without_saving(self) -> None:
160
+ self._quit_without_saving = True
161
+ self.exit()
162
+
163
+ def on_trust_folder_dialog_trusted(self, _: TrustFolderDialog.Trusted) -> None:
164
+ self._result = True
165
+ self.exit()
166
+
167
+ def on_trust_folder_dialog_untrusted(self, _: TrustFolderDialog.Untrusted) -> None:
168
+ self._result = False
169
+ self.exit()
170
+
171
+ def run_trust_dialog(self) -> bool | None:
172
+ self.run()
173
+ if self._quit_without_saving:
174
+ raise TrustDialogQuitException()
175
+ return self._result
176
+
177
+
178
+ def ask_trust_folder(folder_path: Path) -> bool | None:
179
+ app = TrustFolderApp(folder_path)
180
+ return app.run_trust_dialog()
@@ -0,0 +1,83 @@
1
+ Screen {
2
+ align: center middle;
3
+ background: transparent 80%;
4
+ }
5
+
6
+ #trust-dialog {
7
+ width: 70;
8
+ max-width: 90%;
9
+ min-width: 50;
10
+ height: auto;
11
+ border: round ansi_bright_black;
12
+ background: transparent;
13
+ padding: 1;
14
+ }
15
+
16
+ #trust-dialog-title {
17
+ width: 100%;
18
+ height: auto;
19
+ text-style: bold;
20
+ color: ansi_yellow;
21
+ text-align: center;
22
+ margin-bottom: 1;
23
+ }
24
+
25
+ #trust-dialog-path {
26
+ width: 100%;
27
+ height: auto;
28
+ color: ansi_blue;
29
+ text-align: center;
30
+ text-wrap: wrap;
31
+ margin-bottom: 1;
32
+ }
33
+
34
+ #trust-dialog-message {
35
+ width: 100%;
36
+ height: auto;
37
+ color: ansi_default;
38
+ text-align: center;
39
+ text-wrap: wrap;
40
+ margin-bottom: 1;
41
+ }
42
+
43
+ #trust-options-container {
44
+ width: 100%;
45
+ height: auto;
46
+ align: center middle;
47
+ margin-bottom: 1;
48
+ }
49
+
50
+ .trust-option {
51
+ height: auto;
52
+ width: auto;
53
+ color: ansi_default;
54
+ margin: 0 3;
55
+ content-align: center middle;
56
+ }
57
+
58
+ .trust-cursor-selected {
59
+ color: ansi_default;
60
+ text-style: bold;
61
+ }
62
+
63
+ .trust-option-selected {
64
+ color: ansi_default;
65
+ }
66
+
67
+ .trust-dialog-help {
68
+ width: 100%;
69
+ height: auto;
70
+ color: ansi_bright_black;
71
+ text-align: center;
72
+ margin-bottom: 1;
73
+ }
74
+
75
+ .trust-dialog-save-info {
76
+ width: 100%;
77
+ height: auto;
78
+ color: ansi_bright_black;
79
+ text-align: center;
80
+ text-style: italic;
81
+ text-wrap: wrap;
82
+ margin-bottom: 1;
83
+ }
vibe/whats_new.md ADDED
@@ -0,0 +1,5 @@
1
+ # What's New in 2.2.0
2
+
3
+ - **Agent Skills standard** — Vibe now discovers skills from `.agents/skills/` (agentskills.io) as well as `.vibe/skills/`.
4
+
5
+ Optional: usage and tool events are sent to our datalake to improve the product if you have a valid Mistral API key; set `disable_telemetry = true` in config to opt out.