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