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,32 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+ import shlex
6
+ import subprocess
7
+ import tempfile
8
+
9
+
10
+ class ExternalEditor:
11
+ """Handles opening an external editor to edit prompt content."""
12
+
13
+ @staticmethod
14
+ def get_editor() -> str:
15
+ return os.environ.get("VISUAL") or os.environ.get("EDITOR") or "nano"
16
+
17
+ def edit(self, initial_content: str = "") -> str | None:
18
+ editor = self.get_editor()
19
+ fd, filepath = tempfile.mkstemp(suffix=".md", prefix="vibe_")
20
+ try:
21
+ with os.fdopen(fd, "w") as f:
22
+ f.write(initial_content)
23
+
24
+ parts = shlex.split(editor)
25
+ subprocess.run([*parts, filepath], check=True)
26
+
27
+ content = Path(filepath).read_text().rstrip()
28
+ return content if content != initial_content else None
29
+ except (OSError, subprocess.CalledProcessError):
30
+ return
31
+ finally:
32
+ Path(filepath).unlink(missing_ok=True)
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from vibe.cli.textual_ui.handlers.event_handler import EventHandler
4
+
5
+ __all__ = ["EventHandler"]
@@ -0,0 +1,147 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import TYPE_CHECKING
5
+
6
+ from vibe.cli.textual_ui.widgets.compact import CompactMessage
7
+ from vibe.cli.textual_ui.widgets.messages import AssistantMessage, ReasoningMessage
8
+ from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
9
+ from vibe.cli.textual_ui.widgets.tools import ToolCallMessage, ToolResultMessage
10
+ from vibe.core.tools.ui import ToolUIDataAdapter
11
+ from vibe.core.types import (
12
+ AssistantEvent,
13
+ BaseEvent,
14
+ CompactEndEvent,
15
+ CompactStartEvent,
16
+ ReasoningEvent,
17
+ ToolCallEvent,
18
+ ToolResultEvent,
19
+ ToolStreamEvent,
20
+ UserMessageEvent,
21
+ )
22
+ from vibe.core.utils import TaggedText
23
+
24
+ if TYPE_CHECKING:
25
+ from vibe.cli.textual_ui.widgets.loading import LoadingWidget
26
+
27
+
28
+ class EventHandler:
29
+ def __init__(
30
+ self,
31
+ mount_callback: Callable,
32
+ scroll_callback: Callable,
33
+ get_tools_collapsed: Callable[[], bool],
34
+ ) -> None:
35
+ self.mount_callback = mount_callback
36
+ self.scroll_callback = scroll_callback
37
+ self.get_tools_collapsed = get_tools_collapsed
38
+ self.current_tool_call: ToolCallMessage | None = None
39
+ self.current_compact: CompactMessage | None = None
40
+
41
+ async def handle_event(
42
+ self,
43
+ event: BaseEvent,
44
+ loading_active: bool = False,
45
+ loading_widget: LoadingWidget | None = None,
46
+ ) -> ToolCallMessage | None:
47
+ match event:
48
+ case ToolCallEvent():
49
+ return await self._handle_tool_call(event, loading_widget)
50
+ case ToolResultEvent():
51
+ sanitized_event = self._sanitize_event(event)
52
+ await self._handle_tool_result(sanitized_event)
53
+ case ToolStreamEvent():
54
+ await self._handle_tool_stream(event)
55
+ case ReasoningEvent():
56
+ await self._handle_reasoning_message(event)
57
+ case AssistantEvent():
58
+ await self._handle_assistant_message(event)
59
+ case CompactStartEvent():
60
+ await self._handle_compact_start()
61
+ case CompactEndEvent():
62
+ await self._handle_compact_end(event)
63
+ case UserMessageEvent():
64
+ pass
65
+ case _:
66
+ await self._handle_unknown_event(event)
67
+ return None
68
+
69
+ def _sanitize_event(self, event: ToolResultEvent) -> ToolResultEvent:
70
+ if isinstance(event, ToolResultEvent):
71
+ return ToolResultEvent(
72
+ tool_name=event.tool_name,
73
+ tool_class=event.tool_class,
74
+ result=event.result,
75
+ error=TaggedText.from_string(event.error).message
76
+ if event.error
77
+ else None,
78
+ skipped=event.skipped,
79
+ skip_reason=TaggedText.from_string(event.skip_reason).message
80
+ if event.skip_reason
81
+ else None,
82
+ duration=event.duration,
83
+ tool_call_id=event.tool_call_id,
84
+ )
85
+ return event
86
+
87
+ async def _handle_tool_call(
88
+ self, event: ToolCallEvent, loading_widget: LoadingWidget | None = None
89
+ ) -> ToolCallMessage | None:
90
+ tool_call = ToolCallMessage(event)
91
+
92
+ if loading_widget and event.tool_class:
93
+ adapter = ToolUIDataAdapter(event.tool_class)
94
+ status_text = adapter.get_status_text()
95
+ loading_widget.set_status(status_text)
96
+
97
+ self.current_tool_call = tool_call
98
+ await self.mount_callback(tool_call)
99
+
100
+ return tool_call
101
+
102
+ async def _handle_tool_result(self, event: ToolResultEvent) -> None:
103
+ tools_collapsed = self.get_tools_collapsed()
104
+ tool_result = ToolResultMessage(
105
+ event, self.current_tool_call, collapsed=tools_collapsed
106
+ )
107
+ await self.mount_callback(tool_result)
108
+
109
+ self.current_tool_call = None
110
+
111
+ async def _handle_tool_stream(self, event: ToolStreamEvent) -> None:
112
+ if self.current_tool_call:
113
+ self.current_tool_call.set_stream_message(event.message)
114
+
115
+ async def _handle_assistant_message(self, event: AssistantEvent) -> None:
116
+ await self.mount_callback(AssistantMessage(event.content))
117
+
118
+ async def _handle_reasoning_message(self, event: ReasoningEvent) -> None:
119
+ tools_collapsed = self.get_tools_collapsed()
120
+ await self.mount_callback(
121
+ ReasoningMessage(event.content, collapsed=tools_collapsed)
122
+ )
123
+
124
+ async def _handle_compact_start(self) -> None:
125
+ compact_msg = CompactMessage()
126
+ self.current_compact = compact_msg
127
+ await self.mount_callback(compact_msg)
128
+
129
+ async def _handle_compact_end(self, event: CompactEndEvent) -> None:
130
+ if self.current_compact:
131
+ self.current_compact.set_complete(
132
+ old_tokens=event.old_context_tokens, new_tokens=event.new_context_tokens
133
+ )
134
+ self.current_compact = None
135
+
136
+ async def _handle_unknown_event(self, event: BaseEvent) -> None:
137
+ await self.mount_callback(NoMarkupStatic(str(event), classes="unknown-event"))
138
+
139
+ def stop_current_tool_call(self, success: bool = True) -> None:
140
+ if self.current_tool_call:
141
+ self.current_tool_call.stop_spinning(success=success)
142
+ self.current_tool_call = None
143
+
144
+ def stop_current_compact(self) -> None:
145
+ if self.current_compact:
146
+ self.current_compact.stop_spinning(success=False)
147
+ self.current_compact = None
File without changes
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import ClassVar
4
+
5
+ from pydantic import BaseModel
6
+ from textual import events
7
+ from textual.app import ComposeResult
8
+ from textual.binding import Binding, BindingType
9
+ from textual.containers import Container, Vertical, VerticalScroll
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.cli.textual_ui.widgets.tool_widgets import get_approval_widget
15
+ from vibe.core.config import VibeConfig
16
+
17
+
18
+ class ApprovalApp(Container):
19
+ can_focus = True
20
+ can_focus_children = False
21
+
22
+ BINDINGS: ClassVar[list[BindingType]] = [
23
+ Binding("up", "move_up", "Up", show=False),
24
+ Binding("down", "move_down", "Down", show=False),
25
+ Binding("enter", "select", "Select", show=False),
26
+ Binding("1", "select_1", "Yes", show=False),
27
+ Binding("y", "select_1", "Yes", show=False),
28
+ Binding("2", "select_2", "Always Tool Session", show=False),
29
+ Binding("3", "select_3", "No", show=False),
30
+ Binding("n", "select_3", "No", show=False),
31
+ ]
32
+
33
+ class ApprovalGranted(Message):
34
+ def __init__(self, tool_name: str, tool_args: BaseModel) -> None:
35
+ super().__init__()
36
+ self.tool_name = tool_name
37
+ self.tool_args = tool_args
38
+
39
+ class ApprovalGrantedAlwaysTool(Message):
40
+ def __init__(
41
+ self, tool_name: str, tool_args: BaseModel, save_permanently: bool
42
+ ) -> None:
43
+ super().__init__()
44
+ self.tool_name = tool_name
45
+ self.tool_args = tool_args
46
+ self.save_permanently = save_permanently
47
+
48
+ class ApprovalRejected(Message):
49
+ def __init__(self, tool_name: str, tool_args: BaseModel) -> None:
50
+ super().__init__()
51
+ self.tool_name = tool_name
52
+ self.tool_args = tool_args
53
+
54
+ def __init__(
55
+ self, tool_name: str, tool_args: BaseModel, config: VibeConfig
56
+ ) -> None:
57
+ super().__init__(id="approval-app")
58
+ self.tool_name = tool_name
59
+ self.tool_args = tool_args
60
+ self.config = config
61
+ self.selected_option = 0
62
+ self.content_container: Vertical | None = None
63
+ self.title_widget: Static | None = None
64
+ self.tool_info_container: Vertical | None = None
65
+ self.option_widgets: list[Static] = []
66
+ self.help_widget: Static | None = None
67
+
68
+ def compose(self) -> ComposeResult:
69
+ with Vertical(id="approval-options"):
70
+ yield NoMarkupStatic("")
71
+ for _ in range(3):
72
+ widget = NoMarkupStatic("", classes="approval-option")
73
+ self.option_widgets.append(widget)
74
+ yield widget
75
+ yield NoMarkupStatic("")
76
+ self.help_widget = NoMarkupStatic(
77
+ "↑↓ navigate Enter select ESC reject", classes="approval-help"
78
+ )
79
+ yield self.help_widget
80
+
81
+ with Vertical(id="approval-content"):
82
+ self.title_widget = NoMarkupStatic(
83
+ f"⚠ {self.tool_name} command", classes="approval-title"
84
+ )
85
+ yield self.title_widget
86
+
87
+ with VerticalScroll(classes="approval-tool-info-scroll"):
88
+ self.tool_info_container = Vertical(
89
+ classes="approval-tool-info-container"
90
+ )
91
+ yield self.tool_info_container
92
+
93
+ async def on_mount(self) -> None:
94
+ await self._update_tool_info()
95
+ self._update_options()
96
+ self.focus()
97
+
98
+ async def _update_tool_info(self) -> None:
99
+ if not self.tool_info_container:
100
+ return
101
+
102
+ approval_widget = get_approval_widget(self.tool_name, self.tool_args)
103
+ await self.tool_info_container.remove_children()
104
+ await self.tool_info_container.mount(approval_widget)
105
+
106
+ def _update_options(self) -> None:
107
+ options = [
108
+ ("Yes", "yes"),
109
+ (f"Yes and always allow {self.tool_name} for this session", "yes"),
110
+ ("No and tell the agent what to do instead", "no"),
111
+ ]
112
+
113
+ for idx, ((text, color_type), widget) in enumerate(
114
+ zip(options, self.option_widgets, strict=True)
115
+ ):
116
+ is_selected = idx == self.selected_option
117
+
118
+ cursor = "› " if is_selected else " "
119
+ option_text = f"{cursor}{idx + 1}. {text}"
120
+
121
+ widget.update(option_text)
122
+
123
+ widget.remove_class("approval-cursor-selected")
124
+ widget.remove_class("approval-option-selected")
125
+ widget.remove_class("approval-option-yes")
126
+ widget.remove_class("approval-option-no")
127
+
128
+ if is_selected:
129
+ widget.add_class("approval-cursor-selected")
130
+ if color_type == "yes":
131
+ widget.add_class("approval-option-yes")
132
+ else:
133
+ widget.add_class("approval-option-no")
134
+ else:
135
+ widget.add_class("approval-option-selected")
136
+ if color_type == "yes":
137
+ widget.add_class("approval-option-yes")
138
+ else:
139
+ widget.add_class("approval-option-no")
140
+
141
+ def action_move_up(self) -> None:
142
+ self.selected_option = (self.selected_option - 1) % 3
143
+ self._update_options()
144
+
145
+ def action_move_down(self) -> None:
146
+ self.selected_option = (self.selected_option + 1) % 3
147
+ self._update_options()
148
+
149
+ def action_select(self) -> None:
150
+ self._handle_selection(self.selected_option)
151
+
152
+ def action_select_1(self) -> None:
153
+ self.selected_option = 0
154
+ self._handle_selection(0)
155
+
156
+ def action_select_2(self) -> None:
157
+ self.selected_option = 1
158
+ self._handle_selection(1)
159
+
160
+ def action_select_3(self) -> None:
161
+ self.selected_option = 2
162
+ self._handle_selection(2)
163
+
164
+ def action_reject(self) -> None:
165
+ self.selected_option = 2
166
+ self._handle_selection(2)
167
+
168
+ def _handle_selection(self, option: int) -> None:
169
+ match option:
170
+ case 0:
171
+ self.post_message(
172
+ self.ApprovalGranted(
173
+ tool_name=self.tool_name, tool_args=self.tool_args
174
+ )
175
+ )
176
+ case 1:
177
+ self.post_message(
178
+ self.ApprovalGrantedAlwaysTool(
179
+ tool_name=self.tool_name,
180
+ tool_args=self.tool_args,
181
+ save_permanently=False,
182
+ )
183
+ )
184
+ case 2:
185
+ self.post_message(
186
+ self.ApprovalRejected(
187
+ tool_name=self.tool_name, tool_args=self.tool_args
188
+ )
189
+ )
190
+
191
+ def on_blur(self, event: events.Blur) -> None:
192
+ self.call_after_refresh(self.focus)
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from textual.app import ComposeResult
7
+ from textual.containers import Horizontal, Vertical
8
+ from textual.reactive import reactive
9
+ from textual.widgets import Static
10
+
11
+ from vibe import __version__
12
+ from vibe.cli.textual_ui.widgets.banner.petit_chat import PetitChat
13
+ from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
14
+ from vibe.core.config import VibeConfig
15
+ from vibe.core.skills.manager import SkillManager
16
+
17
+
18
+ @dataclass
19
+ class BannerState:
20
+ active_model: str = ""
21
+ models_count: int = 0
22
+ mcp_servers_count: int = 0
23
+ skills_count: int = 0
24
+
25
+
26
+ class Banner(Static):
27
+ state = reactive(BannerState(), init=False)
28
+
29
+ def __init__(
30
+ self, config: VibeConfig, skill_manager: SkillManager, **kwargs: Any
31
+ ) -> None:
32
+ super().__init__(**kwargs)
33
+ self.can_focus = False
34
+ self._initial_state = BannerState(
35
+ active_model=config.active_model,
36
+ models_count=len(config.models),
37
+ mcp_servers_count=len(config.mcp_servers),
38
+ skills_count=len(skill_manager.available_skills),
39
+ )
40
+ self._animated = not config.disable_welcome_banner_animation
41
+
42
+ def compose(self) -> ComposeResult:
43
+ with Horizontal(id="banner-container"):
44
+ yield PetitChat(animate=self._animated)
45
+
46
+ with Vertical(id="banner-info"):
47
+ with Horizontal(classes="banner-line"):
48
+ yield NoMarkupStatic("codeMaster", id="banner-brand")
49
+ yield NoMarkupStatic(" ", classes="banner-spacer")
50
+ yield NoMarkupStatic(f"v{__version__} · ", classes="banner-meta")
51
+ yield NoMarkupStatic("", id="banner-model")
52
+ with Horizontal(classes="banner-line"):
53
+ yield NoMarkupStatic("", id="banner-meta-counts")
54
+ with Horizontal(classes="banner-line"):
55
+ yield NoMarkupStatic("Type ", classes="banner-meta")
56
+ yield NoMarkupStatic("/help", classes="banner-cmd")
57
+ yield NoMarkupStatic(" for more information", classes="banner-meta")
58
+
59
+ def on_mount(self) -> None:
60
+ self.state = self._initial_state
61
+
62
+ def watch_state(self) -> None:
63
+ self.query_one("#banner-model", NoMarkupStatic).update(self.state.active_model)
64
+ self.query_one("#banner-meta-counts", NoMarkupStatic).update(
65
+ self._format_meta_counts()
66
+ )
67
+
68
+ def freeze_animation(self) -> None:
69
+ if self._animated:
70
+ self.query_one(PetitChat).freeze_animation()
71
+
72
+ def set_state(self, config: VibeConfig, skill_manager: SkillManager) -> None:
73
+ self.state = BannerState(
74
+ active_model=config.active_model,
75
+ models_count=len(config.models),
76
+ mcp_servers_count=len(config.mcp_servers),
77
+ skills_count=len(skill_manager.available_skills),
78
+ )
79
+
80
+ def _format_meta_counts(self) -> str:
81
+ return (
82
+ f"{self.state.models_count} model{'s' if self.state.models_count != 1 else ''}"
83
+ f" · {self.state.mcp_servers_count} MCP server{'s' if self.state.mcp_servers_count != 1 else ''}"
84
+ f" · {self.state.skills_count} skill{'s' if self.state.skills_count != 1 else ''}"
85
+ )
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.timer import Timer
7
+ from textual.widgets import Static
8
+
9
+ from vibe.cli.textual_ui.widgets.braille_renderer import render_braille
10
+
11
+ WIDTH = 22
12
+ HEIGHT = 12
13
+ STARTING_DOTS = [
14
+ set[int](),
15
+ {6, 7, 15, 19},
16
+ {5, 8, 14, 16, 18, 20},
17
+ {4, 6, 7, 14, 17, 20},
18
+ {3, 5, 10, 11, 12, 14, 20},
19
+ {3, 5, 9, 13, 14, 16, 18, 20},
20
+ {3, 5, 8, 13, 17, 21},
21
+ {3, 6, 7, 8, 11, 14, 15, 16, 18, 19, 20},
22
+ {4, 5, 8, 12, 17, 19},
23
+ {6, 7, 8, 13, 18, 20},
24
+ {9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
25
+ set[int](),
26
+ ]
27
+ QUEUE_RIGHT_TO_MID = {
28
+ "remove": {1j + 6, 1j + 7, 2j + 8, 3j + 4, 3j + 6, 3j + 7, 8j + 4, 8j + 5},
29
+ "add": {1j + 4, 2j + 3, 3j + 3, 3j + 5, 7j + 5, 8j + 3, 9j + 4, 9j + 5},
30
+ }
31
+ QUEUE_MID_TO_RIGHT = {
32
+ "remove": QUEUE_RIGHT_TO_MID["add"],
33
+ "add": QUEUE_RIGHT_TO_MID["remove"],
34
+ }
35
+ QUEUE_MID_TO_LEFT = {
36
+ "remove": {1j + 4, 2j + 5, 3j + 3, 3j + 5, 7j + 5, 8j + 3, 9j + 4, 9j + 5},
37
+ "add": {1j + 1, 1j + 2, 2j, 3j + 1, 3j + 2, 3j + 4, 8j + 4, 8j + 5},
38
+ }
39
+ QUEUE_LEFT_TO_MID = {
40
+ "remove": QUEUE_MID_TO_LEFT["add"],
41
+ "add": QUEUE_MID_TO_LEFT["remove"],
42
+ }
43
+ WAIT = {"remove": set[int](), "add": set[int]()}
44
+ HEAD_RIGHT = {"remove": {5j + 16, 5j + 18, 6j + 17}, "add": {5j + 17, 5j + 19, 6j + 18}}
45
+ HEAD_LEFT = {"remove": {5j + 17, 5j + 19, 6j + 18}, "add": {5j + 16, 5j + 18, 6j + 17}}
46
+ HEAD_DOWN = {
47
+ "remove": {
48
+ 1j + 15,
49
+ 1j + 19,
50
+ 2j + 14,
51
+ 2j + 16,
52
+ 2j + 18,
53
+ 2j + 20,
54
+ 3j + 17,
55
+ 5j + 17,
56
+ 5j + 19,
57
+ 6j + 13,
58
+ 6j + 18,
59
+ 6j + 21,
60
+ 7j + 14,
61
+ 7j + 15,
62
+ 7j + 16,
63
+ 7j + 19,
64
+ 7j + 20,
65
+ },
66
+ "add": {
67
+ 2j + 15,
68
+ 2j + 19,
69
+ 3j + 16,
70
+ 3j + 18,
71
+ 4j + 17,
72
+ 6j + 14,
73
+ 6j + 17,
74
+ 6j + 19,
75
+ 6j + 20,
76
+ 7j + 13,
77
+ 7j + 18,
78
+ 7j + 21,
79
+ 8j + 14,
80
+ 8j + 15,
81
+ 8j + 16,
82
+ 8j + 18,
83
+ 8j + 20,
84
+ },
85
+ }
86
+ HEAD_UP = {
87
+ "remove": {
88
+ 2j + 15,
89
+ 2j + 19,
90
+ 3j + 16,
91
+ 3j + 18,
92
+ 4j + 17,
93
+ 6j + 14,
94
+ 6j + 17,
95
+ 6j + 19,
96
+ 6j + 20,
97
+ 7j + 13,
98
+ 7j + 18,
99
+ 7j + 21,
100
+ 8j + 14,
101
+ 8j + 15,
102
+ 8j + 16,
103
+ 8j + 18,
104
+ 8j + 20,
105
+ },
106
+ "add": {
107
+ 1j + 15,
108
+ 1j + 19,
109
+ 2j + 14,
110
+ 2j + 16,
111
+ 2j + 18,
112
+ 2j + 20,
113
+ 3j + 17,
114
+ 5j + 17,
115
+ 5j + 19,
116
+ 6j + 13,
117
+ 6j + 18,
118
+ 6j + 21,
119
+ 7j + 14,
120
+ 7j + 15,
121
+ 7j + 16,
122
+ 7j + 18,
123
+ 7j + 19,
124
+ 7j + 20,
125
+ },
126
+ }
127
+ BLINK_EYES_HEAD_HIGH = [
128
+ {"remove": {5j + 16, 5j + 18}, "add": set[int]()},
129
+ {"remove": set[int](), "add": {5j + 16, 5j + 18}},
130
+ ]
131
+ BLINK_EYES_HEAD_LOW = [
132
+ {"remove": {6j + 17, 6j + 19}, "add": set[int]()},
133
+ {"remove": set[int](), "add": {6j + 17, 6j + 19}},
134
+ ]
135
+ TRANSITIONS = [
136
+ *BLINK_EYES_HEAD_HIGH,
137
+ WAIT,
138
+ QUEUE_RIGHT_TO_MID,
139
+ HEAD_RIGHT,
140
+ WAIT,
141
+ QUEUE_MID_TO_LEFT,
142
+ WAIT,
143
+ QUEUE_LEFT_TO_MID,
144
+ WAIT,
145
+ HEAD_DOWN,
146
+ WAIT,
147
+ QUEUE_MID_TO_RIGHT,
148
+ *BLINK_EYES_HEAD_LOW,
149
+ WAIT,
150
+ QUEUE_RIGHT_TO_MID,
151
+ WAIT,
152
+ QUEUE_MID_TO_LEFT,
153
+ WAIT,
154
+ HEAD_UP,
155
+ WAIT,
156
+ QUEUE_LEFT_TO_MID,
157
+ HEAD_LEFT,
158
+ WAIT,
159
+ QUEUE_MID_TO_RIGHT,
160
+ ]
161
+ # cf render_braille() docstring for coordinates convention
162
+
163
+
164
+ class PetitChat(Static):
165
+ def __init__(self, animate: bool = True, **kwargs: Any) -> None:
166
+ super().__init__(**kwargs, classes="banner-chat")
167
+ self._dots = {1j * y + x for y, row in enumerate(STARTING_DOTS) for x in row}
168
+ self._transition_index = 0
169
+ self._do_animate = animate
170
+ self._freeze_requested = False
171
+ self._timer: Timer | None = None
172
+
173
+ def compose(self) -> ComposeResult:
174
+ yield Static(render_braille(self._dots, WIDTH, HEIGHT), classes="petit-chat")
175
+
176
+ def on_mount(self) -> None:
177
+ self._inner = self.query_one(".petit-chat", Static)
178
+ if self._do_animate:
179
+ self._timer = self.set_interval(0.16, self._apply_next_transition)
180
+
181
+ def freeze_animation(self) -> None:
182
+ self._freeze_requested = True
183
+
184
+ def _apply_next_transition(self) -> None:
185
+ if self._freeze_requested and self._transition_index == 0:
186
+ if self._timer:
187
+ self._timer.stop()
188
+ self._timer = None
189
+ return
190
+
191
+ transition = TRANSITIONS[self._transition_index]
192
+ self._dots -= transition["remove"]
193
+ self._dots |= transition["add"]
194
+ self._transition_index = (self._transition_index + 1) % len(TRANSITIONS)
195
+ self._inner.update(render_braille(self._dots, WIDTH, HEIGHT))