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,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.
|