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,201 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import random
|
|
7
|
+
from time import time
|
|
8
|
+
from typing import ClassVar
|
|
9
|
+
|
|
10
|
+
from textual.app import ComposeResult
|
|
11
|
+
from textual.containers import Horizontal
|
|
12
|
+
from textual.widgets import Static
|
|
13
|
+
|
|
14
|
+
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
|
15
|
+
from vibe.cli.textual_ui.widgets.spinner import SpinnerMixin, SpinnerType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _format_elapsed(seconds: int) -> str:
|
|
19
|
+
if seconds < 60: # noqa: PLR2004
|
|
20
|
+
return f"{seconds}s"
|
|
21
|
+
|
|
22
|
+
minutes, secs = divmod(seconds, 60)
|
|
23
|
+
if minutes < 60: # noqa: PLR2004
|
|
24
|
+
return f"{minutes}m{secs}s"
|
|
25
|
+
|
|
26
|
+
hours, mins = divmod(minutes, 60)
|
|
27
|
+
return f"{hours}h{mins}m{secs}s"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LoadingWidget(SpinnerMixin, Static):
|
|
31
|
+
TARGET_COLORS = ("#FFD800", "#FFAF00", "#FF8205", "#FA500F", "#E10500")
|
|
32
|
+
SPINNER_TYPE = SpinnerType.SNAKE
|
|
33
|
+
|
|
34
|
+
EASTER_EGGS: ClassVar[list[str]] = [
|
|
35
|
+
"Eating a chocolatine",
|
|
36
|
+
"Eating a pain au chocolat",
|
|
37
|
+
"Réflexion",
|
|
38
|
+
"Analyse",
|
|
39
|
+
"Contemplation",
|
|
40
|
+
"Synthèse",
|
|
41
|
+
"Reading Proust",
|
|
42
|
+
"Oui oui baguette",
|
|
43
|
+
"Counting Rs in strawberry",
|
|
44
|
+
"Seeding Mistral weights",
|
|
45
|
+
"Vibing",
|
|
46
|
+
"Sending good vibes",
|
|
47
|
+
"Petting le chat",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
EASTER_EGGS_HALLOWEEN: ClassVar[list[str]] = [
|
|
51
|
+
"Trick or treating",
|
|
52
|
+
"Carving pumpkins",
|
|
53
|
+
"Summoning spirits",
|
|
54
|
+
"Brewing potions",
|
|
55
|
+
"Haunting the terminal",
|
|
56
|
+
"Petting le chat noir",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
EASTER_EGGS_DECEMBER: ClassVar[list[str]] = [
|
|
60
|
+
"Wrapping presents",
|
|
61
|
+
"Decorating the tree",
|
|
62
|
+
"Drinking hot chocolate",
|
|
63
|
+
"Building snowmen",
|
|
64
|
+
"Writing holiday cards",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
def __init__(self, status: str | None = None) -> None:
|
|
68
|
+
super().__init__(classes="loading-widget")
|
|
69
|
+
self.init_spinner()
|
|
70
|
+
self.status = status or self._get_default_status()
|
|
71
|
+
self.current_color_index = 0
|
|
72
|
+
self.transition_progress = 0
|
|
73
|
+
self._status_widget: Static | None = None
|
|
74
|
+
self.hint_widget: Static | None = None
|
|
75
|
+
self.start_time: float | None = None
|
|
76
|
+
self._last_elapsed: int = -1
|
|
77
|
+
self._paused_total: float = 0.0
|
|
78
|
+
self._pause_start: float | None = None
|
|
79
|
+
|
|
80
|
+
def _get_easter_egg(self) -> str | None:
|
|
81
|
+
EASTER_EGG_PROBABILITY = 0.10
|
|
82
|
+
if random.random() < EASTER_EGG_PROBABILITY:
|
|
83
|
+
available_eggs = list(self.EASTER_EGGS)
|
|
84
|
+
|
|
85
|
+
OCTOBER = 10
|
|
86
|
+
HALLOWEEN_DAY = 31
|
|
87
|
+
DECEMBER = 12
|
|
88
|
+
now = datetime.now()
|
|
89
|
+
if now.month == OCTOBER and now.day == HALLOWEEN_DAY:
|
|
90
|
+
available_eggs.extend(self.EASTER_EGGS_HALLOWEEN)
|
|
91
|
+
if now.month == DECEMBER:
|
|
92
|
+
available_eggs.extend(self.EASTER_EGGS_DECEMBER)
|
|
93
|
+
|
|
94
|
+
return random.choice(available_eggs)
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def _get_default_status(self) -> str:
|
|
98
|
+
return self._get_easter_egg() or "Generating"
|
|
99
|
+
|
|
100
|
+
def _apply_easter_egg(self, status: str) -> str:
|
|
101
|
+
return self._get_easter_egg() or status
|
|
102
|
+
|
|
103
|
+
def pause_timer(self) -> None:
|
|
104
|
+
if self._pause_start is None:
|
|
105
|
+
self._pause_start = time()
|
|
106
|
+
|
|
107
|
+
def resume_timer(self) -> None:
|
|
108
|
+
if self._pause_start is not None:
|
|
109
|
+
self._paused_total += time() - self._pause_start
|
|
110
|
+
self._pause_start = None
|
|
111
|
+
|
|
112
|
+
def set_status(self, status: str) -> None:
|
|
113
|
+
self.status = self._apply_easter_egg(status)
|
|
114
|
+
self._update_animation()
|
|
115
|
+
|
|
116
|
+
def compose(self) -> ComposeResult:
|
|
117
|
+
with Horizontal(classes="loading-container"):
|
|
118
|
+
self._indicator_widget = Static(
|
|
119
|
+
self._spinner.current_frame(), classes="loading-indicator"
|
|
120
|
+
)
|
|
121
|
+
yield self._indicator_widget
|
|
122
|
+
|
|
123
|
+
self._status_widget = Static("", classes="loading-status")
|
|
124
|
+
yield self._status_widget
|
|
125
|
+
|
|
126
|
+
self.hint_widget = NoMarkupStatic(
|
|
127
|
+
"(0s esc to interrupt)", classes="loading-hint"
|
|
128
|
+
)
|
|
129
|
+
yield self.hint_widget
|
|
130
|
+
|
|
131
|
+
def on_mount(self) -> None:
|
|
132
|
+
self.start_time = time()
|
|
133
|
+
self._update_animation()
|
|
134
|
+
self.start_spinner_timer()
|
|
135
|
+
|
|
136
|
+
def on_resize(self) -> None:
|
|
137
|
+
self.refresh_spinner()
|
|
138
|
+
|
|
139
|
+
def _update_spinner_frame(self) -> None:
|
|
140
|
+
if not self._is_spinning:
|
|
141
|
+
return
|
|
142
|
+
self._update_animation()
|
|
143
|
+
|
|
144
|
+
def _get_color_for_position(self, position: int) -> str:
|
|
145
|
+
current_color = self.TARGET_COLORS[self.current_color_index]
|
|
146
|
+
next_color = self.TARGET_COLORS[
|
|
147
|
+
(self.current_color_index + 1) % len(self.TARGET_COLORS)
|
|
148
|
+
]
|
|
149
|
+
if position < self.transition_progress:
|
|
150
|
+
return next_color
|
|
151
|
+
return current_color
|
|
152
|
+
|
|
153
|
+
def _build_status_text(self) -> str:
|
|
154
|
+
parts = []
|
|
155
|
+
for i, char in enumerate(self.status):
|
|
156
|
+
color = self._get_color_for_position(1 + i)
|
|
157
|
+
parts.append(f"[{color}]{char}[/]")
|
|
158
|
+
ellipsis_start = 1 + len(self.status)
|
|
159
|
+
color_ellipsis = self._get_color_for_position(ellipsis_start)
|
|
160
|
+
parts.append(f"[{color_ellipsis}]… [/]")
|
|
161
|
+
return "".join(parts)
|
|
162
|
+
|
|
163
|
+
def _update_animation(self) -> None:
|
|
164
|
+
total_elements = 1 + len(self.status) + 1
|
|
165
|
+
|
|
166
|
+
if self._indicator_widget:
|
|
167
|
+
spinner_char = self._spinner.next_frame()
|
|
168
|
+
color = self._get_color_for_position(0)
|
|
169
|
+
self._indicator_widget.update(f"[{color}]{spinner_char}[/]")
|
|
170
|
+
|
|
171
|
+
if self._status_widget:
|
|
172
|
+
self._status_widget.update(self._build_status_text())
|
|
173
|
+
|
|
174
|
+
self.transition_progress += 1
|
|
175
|
+
if self.transition_progress > total_elements:
|
|
176
|
+
self.current_color_index = (self.current_color_index + 1) % len(
|
|
177
|
+
self.TARGET_COLORS
|
|
178
|
+
)
|
|
179
|
+
self.transition_progress = 0
|
|
180
|
+
|
|
181
|
+
if self.hint_widget and self.start_time is not None:
|
|
182
|
+
paused = self._paused_total + (
|
|
183
|
+
time() - self._pause_start if self._pause_start else 0
|
|
184
|
+
)
|
|
185
|
+
elapsed = int(time() - self.start_time - paused)
|
|
186
|
+
if elapsed != self._last_elapsed:
|
|
187
|
+
self._last_elapsed = elapsed
|
|
188
|
+
self.hint_widget.update(
|
|
189
|
+
f"({_format_elapsed(elapsed)} esc to interrupt)"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@contextmanager
|
|
194
|
+
def paused_timer(loading_widget: LoadingWidget | None) -> Iterator[None]:
|
|
195
|
+
if loading_widget:
|
|
196
|
+
loading_widget.pause_timer()
|
|
197
|
+
try:
|
|
198
|
+
yield
|
|
199
|
+
finally:
|
|
200
|
+
if loading_widget:
|
|
201
|
+
loading_widget.resume_timer()
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.containers import Horizontal, Vertical
|
|
7
|
+
from textual.widgets import Static
|
|
8
|
+
from textual.widgets._markdown import MarkdownStream
|
|
9
|
+
|
|
10
|
+
from vibe.cli.textual_ui.ansi_markdown import AnsiMarkdown as Markdown
|
|
11
|
+
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
|
12
|
+
from vibe.cli.textual_ui.widgets.spinner import SpinnerMixin, SpinnerType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NonSelectableStatic(NoMarkupStatic):
|
|
16
|
+
@property
|
|
17
|
+
def text_selection(self) -> None:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
@text_selection.setter
|
|
21
|
+
def text_selection(self, value: Any) -> None:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
def get_selection(self, selection: Any) -> None:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ExpandingBorder(NonSelectableStatic):
|
|
29
|
+
def render(self) -> str:
|
|
30
|
+
height = self.size.height
|
|
31
|
+
return "\n".join(["⎢"] * (height - 1) + ["⎣"])
|
|
32
|
+
|
|
33
|
+
def on_resize(self) -> None:
|
|
34
|
+
self.refresh()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UserMessage(Static):
|
|
38
|
+
def __init__(self, content: str, pending: bool = False) -> None:
|
|
39
|
+
super().__init__()
|
|
40
|
+
self.add_class("user-message")
|
|
41
|
+
self._content = content
|
|
42
|
+
self._pending = pending
|
|
43
|
+
|
|
44
|
+
def compose(self) -> ComposeResult:
|
|
45
|
+
with Horizontal(classes="user-message-container"):
|
|
46
|
+
yield NoMarkupStatic(self._content, classes="user-message-content")
|
|
47
|
+
if self._pending:
|
|
48
|
+
self.add_class("pending")
|
|
49
|
+
|
|
50
|
+
async def set_pending(self, pending: bool) -> None:
|
|
51
|
+
if pending == self._pending:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
self._pending = pending
|
|
55
|
+
|
|
56
|
+
if pending:
|
|
57
|
+
self.add_class("pending")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
self.remove_class("pending")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class StreamingMessageBase(Static):
|
|
64
|
+
def __init__(self, content: str) -> None:
|
|
65
|
+
super().__init__()
|
|
66
|
+
self._content = content
|
|
67
|
+
self._markdown: Markdown | None = None
|
|
68
|
+
self._stream: MarkdownStream | None = None
|
|
69
|
+
self._content_initialized = False
|
|
70
|
+
|
|
71
|
+
def _get_markdown(self) -> Markdown:
|
|
72
|
+
if self._markdown is None:
|
|
73
|
+
raise RuntimeError(
|
|
74
|
+
"Markdown widget not initialized. compose() must be called first."
|
|
75
|
+
)
|
|
76
|
+
return self._markdown
|
|
77
|
+
|
|
78
|
+
def _ensure_stream(self) -> MarkdownStream:
|
|
79
|
+
if self._stream is None:
|
|
80
|
+
self._stream = Markdown.get_stream(self._get_markdown())
|
|
81
|
+
return self._stream
|
|
82
|
+
|
|
83
|
+
async def append_content(self, content: str) -> None:
|
|
84
|
+
if not content:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
self._content += content
|
|
88
|
+
if self._should_write_content():
|
|
89
|
+
stream = self._ensure_stream()
|
|
90
|
+
await stream.write(content)
|
|
91
|
+
|
|
92
|
+
async def write_initial_content(self) -> None:
|
|
93
|
+
if self._content_initialized:
|
|
94
|
+
return
|
|
95
|
+
if self._content and self._should_write_content():
|
|
96
|
+
stream = self._ensure_stream()
|
|
97
|
+
await stream.write(self._content)
|
|
98
|
+
|
|
99
|
+
async def stop_stream(self) -> None:
|
|
100
|
+
if self._stream is None:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
await self._stream.stop()
|
|
104
|
+
self._stream = None
|
|
105
|
+
|
|
106
|
+
def _should_write_content(self) -> bool:
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class AssistantMessage(StreamingMessageBase):
|
|
111
|
+
def __init__(self, content: str) -> None:
|
|
112
|
+
super().__init__(content)
|
|
113
|
+
self.add_class("assistant-message")
|
|
114
|
+
|
|
115
|
+
def compose(self) -> ComposeResult:
|
|
116
|
+
if self._content:
|
|
117
|
+
self._content_initialized = True
|
|
118
|
+
markdown = Markdown(self._content)
|
|
119
|
+
self._markdown = markdown
|
|
120
|
+
yield markdown
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ReasoningMessage(SpinnerMixin, StreamingMessageBase):
|
|
124
|
+
SPINNER_TYPE = SpinnerType.PULSE
|
|
125
|
+
SPINNING_TEXT = "Thinking"
|
|
126
|
+
COMPLETED_TEXT = "Thought"
|
|
127
|
+
|
|
128
|
+
def __init__(self, content: str, collapsed: bool = True) -> None:
|
|
129
|
+
super().__init__(content)
|
|
130
|
+
self.add_class("reasoning-message")
|
|
131
|
+
self.collapsed = collapsed
|
|
132
|
+
self._indicator_widget: Static | None = None
|
|
133
|
+
self._triangle_widget: Static | None = None
|
|
134
|
+
self.init_spinner()
|
|
135
|
+
|
|
136
|
+
def compose(self) -> ComposeResult:
|
|
137
|
+
with Vertical(classes="reasoning-message-wrapper"):
|
|
138
|
+
with Horizontal(classes="reasoning-message-header"):
|
|
139
|
+
self._indicator_widget = NonSelectableStatic(
|
|
140
|
+
self._spinner.current_frame(), classes="reasoning-indicator"
|
|
141
|
+
)
|
|
142
|
+
yield self._indicator_widget
|
|
143
|
+
self._status_text_widget = NoMarkupStatic(
|
|
144
|
+
self.SPINNING_TEXT, classes="reasoning-collapsed-text"
|
|
145
|
+
)
|
|
146
|
+
yield self._status_text_widget
|
|
147
|
+
self._triangle_widget = NonSelectableStatic(
|
|
148
|
+
"▶" if self.collapsed else "▼", classes="reasoning-triangle"
|
|
149
|
+
)
|
|
150
|
+
yield self._triangle_widget
|
|
151
|
+
markdown = Markdown("", classes="reasoning-message-content")
|
|
152
|
+
markdown.display = not self.collapsed
|
|
153
|
+
self._markdown = markdown
|
|
154
|
+
yield markdown
|
|
155
|
+
|
|
156
|
+
def on_mount(self) -> None:
|
|
157
|
+
self.start_spinner_timer()
|
|
158
|
+
|
|
159
|
+
def on_resize(self) -> None:
|
|
160
|
+
self.refresh_spinner()
|
|
161
|
+
|
|
162
|
+
async def on_click(self) -> None:
|
|
163
|
+
await self._toggle_collapsed()
|
|
164
|
+
|
|
165
|
+
async def _toggle_collapsed(self) -> None:
|
|
166
|
+
await self.set_collapsed(not self.collapsed)
|
|
167
|
+
|
|
168
|
+
def _should_write_content(self) -> bool:
|
|
169
|
+
return not self.collapsed
|
|
170
|
+
|
|
171
|
+
async def set_collapsed(self, collapsed: bool) -> None:
|
|
172
|
+
if self.collapsed == collapsed:
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
self.collapsed = collapsed
|
|
176
|
+
if self._triangle_widget:
|
|
177
|
+
self._triangle_widget.update("▶" if collapsed else "▼")
|
|
178
|
+
if self._markdown:
|
|
179
|
+
self._markdown.display = not collapsed
|
|
180
|
+
if not collapsed and self._content:
|
|
181
|
+
if self._stream is not None:
|
|
182
|
+
await self._stream.stop()
|
|
183
|
+
self._stream = None
|
|
184
|
+
await self._markdown.update("")
|
|
185
|
+
stream = self._ensure_stream()
|
|
186
|
+
await stream.write(self._content)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class UserCommandMessage(Static):
|
|
190
|
+
def __init__(self, content: str) -> None:
|
|
191
|
+
super().__init__()
|
|
192
|
+
self.add_class("user-command-message")
|
|
193
|
+
self._content = content
|
|
194
|
+
|
|
195
|
+
def compose(self) -> ComposeResult:
|
|
196
|
+
with Horizontal(classes="user-command-container"):
|
|
197
|
+
yield ExpandingBorder(classes="user-command-border")
|
|
198
|
+
with Vertical(classes="user-command-content"):
|
|
199
|
+
yield Markdown(self._content)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class WhatsNewMessage(Static):
|
|
203
|
+
def __init__(self, content: str) -> None:
|
|
204
|
+
super().__init__()
|
|
205
|
+
self.add_class("whats-new-message")
|
|
206
|
+
self._content = content
|
|
207
|
+
|
|
208
|
+
def compose(self) -> ComposeResult:
|
|
209
|
+
yield Markdown(self._content)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class InterruptMessage(Static):
|
|
213
|
+
def __init__(self) -> None:
|
|
214
|
+
super().__init__()
|
|
215
|
+
self.add_class("interrupt-message")
|
|
216
|
+
|
|
217
|
+
def compose(self) -> ComposeResult:
|
|
218
|
+
with Horizontal(classes="interrupt-container"):
|
|
219
|
+
yield ExpandingBorder(classes="interrupt-border")
|
|
220
|
+
yield NoMarkupStatic(
|
|
221
|
+
"Interrupted · What should Vibe do instead?",
|
|
222
|
+
classes="interrupt-content",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class BashOutputMessage(Static):
|
|
227
|
+
def __init__(self, command: str, cwd: str, output: str, exit_code: int) -> None:
|
|
228
|
+
super().__init__()
|
|
229
|
+
self.add_class("bash-output-message")
|
|
230
|
+
self._command = command
|
|
231
|
+
self._cwd = cwd
|
|
232
|
+
self._output = output.rstrip("\n")
|
|
233
|
+
self._exit_code = exit_code
|
|
234
|
+
|
|
235
|
+
def compose(self) -> ComposeResult:
|
|
236
|
+
status_class = "bash-success" if self._exit_code == 0 else "bash-error"
|
|
237
|
+
self.add_class(status_class)
|
|
238
|
+
with Horizontal(classes="bash-command-line"):
|
|
239
|
+
yield NonSelectableStatic("$ ", classes=f"bash-prompt {status_class}")
|
|
240
|
+
yield NoMarkupStatic(self._command, classes="bash-command")
|
|
241
|
+
with Horizontal(classes="bash-output-container"):
|
|
242
|
+
yield ExpandingBorder(classes="bash-output-border")
|
|
243
|
+
yield NoMarkupStatic(self._output, classes="bash-output")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class ErrorMessage(Static):
|
|
247
|
+
def __init__(self, error: str, collapsed: bool = False) -> None:
|
|
248
|
+
super().__init__()
|
|
249
|
+
self.add_class("error-message")
|
|
250
|
+
self._error = error
|
|
251
|
+
self.collapsed = collapsed
|
|
252
|
+
self._content_widget: Static | None = None
|
|
253
|
+
|
|
254
|
+
def compose(self) -> ComposeResult:
|
|
255
|
+
with Horizontal(classes="error-container"):
|
|
256
|
+
yield ExpandingBorder(classes="error-border")
|
|
257
|
+
self._content_widget = NoMarkupStatic(
|
|
258
|
+
f"Error: {self._error}", classes="error-content"
|
|
259
|
+
)
|
|
260
|
+
yield self._content_widget
|
|
261
|
+
|
|
262
|
+
def set_collapsed(self, collapsed: bool) -> None:
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class WarningMessage(Static):
|
|
267
|
+
def __init__(self, message: str, show_border: bool = True) -> None:
|
|
268
|
+
super().__init__()
|
|
269
|
+
self.add_class("warning-message")
|
|
270
|
+
self._message = message
|
|
271
|
+
self._show_border = show_border
|
|
272
|
+
|
|
273
|
+
def compose(self) -> ComposeResult:
|
|
274
|
+
with Horizontal(classes="warning-container"):
|
|
275
|
+
if self._show_border:
|
|
276
|
+
yield ExpandingBorder(classes="warning-border")
|
|
277
|
+
yield NoMarkupStatic(self._message, classes="warning-content")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from textual.visual import VisualType
|
|
6
|
+
from textual.widgets import Static
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NoMarkupStatic(Static):
|
|
10
|
+
def __init__(self, content: VisualType = "", **kwargs: Any) -> None:
|
|
11
|
+
super().__init__(content, markup=False, **kwargs)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PathDisplay(NoMarkupStatic):
|
|
9
|
+
def __init__(self, path: Path | str) -> None:
|
|
10
|
+
super().__init__()
|
|
11
|
+
self.can_focus = False
|
|
12
|
+
self._path = Path(path)
|
|
13
|
+
self._update_display()
|
|
14
|
+
|
|
15
|
+
def _update_display(self) -> None:
|
|
16
|
+
path_str = str(self._path)
|
|
17
|
+
try:
|
|
18
|
+
home = Path.home()
|
|
19
|
+
if self._path.is_relative_to(home):
|
|
20
|
+
path_str = f"~/{self._path.relative_to(home)}"
|
|
21
|
+
except (ValueError, OSError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
self.update(path_str)
|
|
25
|
+
|
|
26
|
+
def set_path(self, path: Path | str) -> None:
|
|
27
|
+
self._path = Path(path)
|
|
28
|
+
self._update_display()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from textual import events
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.binding import Binding, BindingType
|
|
8
|
+
from textual.containers import Container, Vertical
|
|
9
|
+
from textual.message import Message
|
|
10
|
+
from textual.widgets import Input, Static
|
|
11
|
+
|
|
12
|
+
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
|
13
|
+
from vibe.core.proxy_setup import (
|
|
14
|
+
SUPPORTED_PROXY_VARS,
|
|
15
|
+
get_current_proxy_settings,
|
|
16
|
+
set_proxy_var,
|
|
17
|
+
unset_proxy_var,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ProxySetupApp(Container):
|
|
22
|
+
can_focus = True
|
|
23
|
+
can_focus_children = True
|
|
24
|
+
|
|
25
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
26
|
+
Binding("up", "focus_previous", "Up", show=False),
|
|
27
|
+
Binding("down", "focus_next", "Down", show=False),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
class ProxySetupClosed(Message):
|
|
31
|
+
def __init__(self, saved: bool, error: str | None = None) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.saved = saved
|
|
34
|
+
self.error = error
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
super().__init__(id="proxysetup-app")
|
|
38
|
+
self.inputs: dict[str, Input] = {}
|
|
39
|
+
self.initial_values: dict[str, str | None] = {}
|
|
40
|
+
|
|
41
|
+
def compose(self) -> ComposeResult:
|
|
42
|
+
self.initial_values = get_current_proxy_settings()
|
|
43
|
+
|
|
44
|
+
with Vertical(id="proxysetup-content"):
|
|
45
|
+
yield NoMarkupStatic("Proxy Configuration", classes="settings-title")
|
|
46
|
+
yield NoMarkupStatic("")
|
|
47
|
+
|
|
48
|
+
for key, description in SUPPORTED_PROXY_VARS.items():
|
|
49
|
+
yield Static(
|
|
50
|
+
f"[bold ansi_blue]{key}[/] [dim]{description}[/dim]",
|
|
51
|
+
classes="proxy-label-line",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
initial_value = self.initial_values.get(key) or ""
|
|
55
|
+
input_widget = Input(
|
|
56
|
+
value=initial_value,
|
|
57
|
+
placeholder="NOT SET",
|
|
58
|
+
id=f"proxy-input-{key}",
|
|
59
|
+
classes="proxy-input",
|
|
60
|
+
)
|
|
61
|
+
self.inputs[key] = input_widget
|
|
62
|
+
yield input_widget
|
|
63
|
+
|
|
64
|
+
yield NoMarkupStatic("")
|
|
65
|
+
|
|
66
|
+
yield NoMarkupStatic(
|
|
67
|
+
"↑↓ navigate Enter save & exit ESC cancel", classes="settings-help"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def focus(self, scroll_visible: bool = True) -> ProxySetupApp:
|
|
71
|
+
"""Override focus to focus the first input widget."""
|
|
72
|
+
if self.inputs:
|
|
73
|
+
first_input = list(self.inputs.values())[0]
|
|
74
|
+
first_input.focus(scroll_visible=scroll_visible)
|
|
75
|
+
else:
|
|
76
|
+
super().focus(scroll_visible=scroll_visible)
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def action_focus_next(self) -> None:
|
|
80
|
+
inputs = list(self.inputs.values())
|
|
81
|
+
focused = self.screen.focused
|
|
82
|
+
if focused is not None and isinstance(focused, Input) and focused in inputs:
|
|
83
|
+
idx = inputs.index(focused)
|
|
84
|
+
next_idx = (idx + 1) % len(inputs)
|
|
85
|
+
inputs[next_idx].focus()
|
|
86
|
+
|
|
87
|
+
def action_focus_previous(self) -> None:
|
|
88
|
+
inputs = list(self.inputs.values())
|
|
89
|
+
focused = self.screen.focused
|
|
90
|
+
if focused is not None and isinstance(focused, Input) and focused in inputs:
|
|
91
|
+
idx = inputs.index(focused)
|
|
92
|
+
prev_idx = (idx - 1) % len(inputs)
|
|
93
|
+
inputs[prev_idx].focus()
|
|
94
|
+
|
|
95
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
96
|
+
self._save_and_close()
|
|
97
|
+
|
|
98
|
+
def on_blur(self, _event: events.Blur) -> None:
|
|
99
|
+
self.call_after_refresh(self._refocus_if_needed)
|
|
100
|
+
|
|
101
|
+
def on_input_blurred(self, _event: Input.Blurred) -> None:
|
|
102
|
+
self.call_after_refresh(self._refocus_if_needed)
|
|
103
|
+
|
|
104
|
+
def _refocus_if_needed(self) -> None:
|
|
105
|
+
if self.has_focus or any(inp.has_focus for inp in self.inputs.values()):
|
|
106
|
+
return
|
|
107
|
+
self.focus()
|
|
108
|
+
|
|
109
|
+
def _save_and_close(self) -> None:
|
|
110
|
+
try:
|
|
111
|
+
for key, input_widget in self.inputs.items():
|
|
112
|
+
new_value = input_widget.value.strip()
|
|
113
|
+
old_value = self.initial_values.get(key) or ""
|
|
114
|
+
|
|
115
|
+
if new_value != old_value:
|
|
116
|
+
if new_value:
|
|
117
|
+
set_proxy_var(key, new_value)
|
|
118
|
+
else:
|
|
119
|
+
unset_proxy_var(key)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
self.post_message(self.ProxySetupClosed(saved=False, error=str(e)))
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
self.post_message(self.ProxySetupClosed(saved=True))
|
|
125
|
+
|
|
126
|
+
def action_close(self) -> None:
|
|
127
|
+
self.post_message(self.ProxySetupClosed(saved=False))
|