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