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,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from acp.helpers import SessionUpdate, ToolCallContentVariant
|
|
4
|
+
from acp.schema import (
|
|
5
|
+
ContentToolCallContent,
|
|
6
|
+
TextContentBlock,
|
|
7
|
+
ToolCallProgress,
|
|
8
|
+
ToolCallStart,
|
|
9
|
+
ToolKind,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from vibe.acp.tools.base import (
|
|
13
|
+
ToolCallSessionUpdateProtocol,
|
|
14
|
+
ToolResultSessionUpdateProtocol,
|
|
15
|
+
)
|
|
16
|
+
from vibe.core.tools.ui import ToolUIDataAdapter
|
|
17
|
+
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
|
18
|
+
from vibe.core.utils import TaggedText, is_user_cancellation_event
|
|
19
|
+
|
|
20
|
+
TOOL_KIND: dict[str, ToolKind] = {
|
|
21
|
+
"grep": "search",
|
|
22
|
+
"read_file": "read",
|
|
23
|
+
# Right now, jetbrains implementation of "edit" tool kind is broken
|
|
24
|
+
# Leading to the tool not appearing in the chat
|
|
25
|
+
# "write_file": "edit",
|
|
26
|
+
# "search_replace": "edit",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def tool_call_session_update(event: ToolCallEvent) -> SessionUpdate | None:
|
|
31
|
+
if issubclass(event.tool_class, ToolCallSessionUpdateProtocol):
|
|
32
|
+
return event.tool_class.tool_call_session_update(event)
|
|
33
|
+
|
|
34
|
+
adapter = ToolUIDataAdapter(event.tool_class)
|
|
35
|
+
display = adapter.get_call_display(event)
|
|
36
|
+
content: list[ToolCallContentVariant] | None = (
|
|
37
|
+
[
|
|
38
|
+
ContentToolCallContent(
|
|
39
|
+
type="content",
|
|
40
|
+
content=TextContentBlock(type="text", text=display.content),
|
|
41
|
+
)
|
|
42
|
+
]
|
|
43
|
+
if display.content
|
|
44
|
+
else None
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return ToolCallStart(
|
|
48
|
+
session_update="tool_call",
|
|
49
|
+
title=display.summary,
|
|
50
|
+
content=content,
|
|
51
|
+
tool_call_id=event.tool_call_id,
|
|
52
|
+
kind=TOOL_KIND.get(event.tool_name, "other"),
|
|
53
|
+
raw_input=event.args.model_dump_json(),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def tool_result_session_update(event: ToolResultEvent) -> SessionUpdate | None:
|
|
58
|
+
if is_user_cancellation_event(event):
|
|
59
|
+
tool_status = "failed"
|
|
60
|
+
raw_output = (
|
|
61
|
+
TaggedText.from_string(event.skip_reason).message
|
|
62
|
+
if event.skip_reason
|
|
63
|
+
else None
|
|
64
|
+
)
|
|
65
|
+
elif event.result:
|
|
66
|
+
tool_status = "completed"
|
|
67
|
+
raw_output = event.result.model_dump_json()
|
|
68
|
+
else:
|
|
69
|
+
tool_status = "failed"
|
|
70
|
+
raw_output = (
|
|
71
|
+
TaggedText.from_string(event.error).message if event.error else None
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if event.tool_class is None:
|
|
75
|
+
return ToolCallProgress(
|
|
76
|
+
session_update="tool_call_update",
|
|
77
|
+
tool_call_id=event.tool_call_id,
|
|
78
|
+
status="failed",
|
|
79
|
+
raw_output=raw_output,
|
|
80
|
+
content=[
|
|
81
|
+
ContentToolCallContent(
|
|
82
|
+
type="content",
|
|
83
|
+
content=TextContentBlock(type="text", text=raw_output or ""),
|
|
84
|
+
)
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if issubclass(event.tool_class, ToolResultSessionUpdateProtocol):
|
|
89
|
+
return event.tool_class.tool_result_session_update(event)
|
|
90
|
+
|
|
91
|
+
if tool_status == "failed":
|
|
92
|
+
content = [
|
|
93
|
+
ContentToolCallContent(
|
|
94
|
+
type="content",
|
|
95
|
+
content=TextContentBlock(type="text", text=raw_output or ""),
|
|
96
|
+
)
|
|
97
|
+
]
|
|
98
|
+
else:
|
|
99
|
+
adapter = ToolUIDataAdapter(event.tool_class)
|
|
100
|
+
display = adapter.get_result_display(event)
|
|
101
|
+
content: list[ToolCallContentVariant] | None = (
|
|
102
|
+
[
|
|
103
|
+
ContentToolCallContent(
|
|
104
|
+
type="content",
|
|
105
|
+
content=TextContentBlock(type="text", text=display.message),
|
|
106
|
+
)
|
|
107
|
+
]
|
|
108
|
+
if display.message
|
|
109
|
+
else None
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return ToolCallProgress(
|
|
113
|
+
session_update="tool_call_update",
|
|
114
|
+
tool_call_id=event.tool_call_id,
|
|
115
|
+
status=tool_status,
|
|
116
|
+
raw_output=raw_output,
|
|
117
|
+
content=content,
|
|
118
|
+
)
|
vibe/acp/utils.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import TYPE_CHECKING, Literal, cast
|
|
5
|
+
|
|
6
|
+
from acp.schema import (
|
|
7
|
+
AgentMessageChunk,
|
|
8
|
+
AgentThoughtChunk,
|
|
9
|
+
ContentToolCallContent,
|
|
10
|
+
PermissionOption,
|
|
11
|
+
SessionMode,
|
|
12
|
+
TextContentBlock,
|
|
13
|
+
ToolCallProgress,
|
|
14
|
+
ToolCallStart,
|
|
15
|
+
UserMessageChunk,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from vibe.core.agents.models import AgentProfile, AgentType
|
|
19
|
+
from vibe.core.proxy_setup import SUPPORTED_PROXY_VARS, get_current_proxy_settings
|
|
20
|
+
from vibe.core.types import CompactEndEvent, CompactStartEvent, LLMMessage
|
|
21
|
+
from vibe.core.utils import compact_reduction_display
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from vibe.core.agents.manager import AgentManager
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ToolOption(StrEnum):
|
|
28
|
+
ALLOW_ONCE = "allow_once"
|
|
29
|
+
ALLOW_ALWAYS = "allow_always"
|
|
30
|
+
REJECT_ONCE = "reject_once"
|
|
31
|
+
REJECT_ALWAYS = "reject_always"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
TOOL_OPTIONS = [
|
|
35
|
+
PermissionOption(
|
|
36
|
+
option_id=ToolOption.ALLOW_ONCE,
|
|
37
|
+
name="Allow once",
|
|
38
|
+
kind=cast(Literal["allow_once"], ToolOption.ALLOW_ONCE),
|
|
39
|
+
),
|
|
40
|
+
PermissionOption(
|
|
41
|
+
option_id=ToolOption.ALLOW_ALWAYS,
|
|
42
|
+
name="Allow always",
|
|
43
|
+
kind=cast(Literal["allow_always"], ToolOption.ALLOW_ALWAYS),
|
|
44
|
+
),
|
|
45
|
+
PermissionOption(
|
|
46
|
+
option_id=ToolOption.REJECT_ONCE,
|
|
47
|
+
name="Reject once",
|
|
48
|
+
kind=cast(Literal["reject_once"], ToolOption.REJECT_ONCE),
|
|
49
|
+
),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def agent_profile_to_acp(profile: AgentProfile) -> SessionMode:
|
|
54
|
+
return SessionMode(
|
|
55
|
+
id=profile.name, name=profile.display_name, description=profile.description
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_valid_acp_agent(agent_manager: AgentManager, agent_name: str) -> bool:
|
|
60
|
+
return agent_name in agent_manager.available_agents
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_all_acp_session_modes(agent_manager: AgentManager) -> list[SessionMode]:
|
|
64
|
+
return [
|
|
65
|
+
agent_profile_to_acp(profile)
|
|
66
|
+
for profile in agent_manager.available_agents.values()
|
|
67
|
+
if profile.agent_type == AgentType.AGENT
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def create_compact_start_session_update(event: CompactStartEvent) -> ToolCallStart:
|
|
72
|
+
# WORKAROUND: Using tool_call to communicate compact events to the client.
|
|
73
|
+
# This should be revisited when the ACP protocol defines how compact events
|
|
74
|
+
# should be represented.
|
|
75
|
+
# [RFD](https://agentclientprotocol.com/rfds/session-usage)
|
|
76
|
+
return ToolCallStart(
|
|
77
|
+
session_update="tool_call",
|
|
78
|
+
tool_call_id=event.tool_call_id,
|
|
79
|
+
title="Compacting conversation history...",
|
|
80
|
+
kind="other",
|
|
81
|
+
status="in_progress",
|
|
82
|
+
content=[
|
|
83
|
+
ContentToolCallContent(
|
|
84
|
+
type="content",
|
|
85
|
+
content=TextContentBlock(
|
|
86
|
+
type="text",
|
|
87
|
+
text="Automatic context management, no approval required. This may take some time...",
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def create_compact_end_session_update(event: CompactEndEvent) -> ToolCallProgress:
|
|
95
|
+
# WORKAROUND: Using tool_call_update to communicate compact events to the client.
|
|
96
|
+
# This should be revisited when the ACP protocol defines how compact events
|
|
97
|
+
# should be represented.
|
|
98
|
+
# [RFD](https://agentclientprotocol.com/rfds/session-usage)
|
|
99
|
+
return ToolCallProgress(
|
|
100
|
+
session_update="tool_call_update",
|
|
101
|
+
tool_call_id=event.tool_call_id,
|
|
102
|
+
title="Compacted conversation history",
|
|
103
|
+
status="completed",
|
|
104
|
+
content=[
|
|
105
|
+
ContentToolCallContent(
|
|
106
|
+
type="content",
|
|
107
|
+
content=TextContentBlock(
|
|
108
|
+
type="text",
|
|
109
|
+
text=(
|
|
110
|
+
compact_reduction_display(
|
|
111
|
+
event.old_context_tokens, event.new_context_tokens
|
|
112
|
+
)
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_proxy_help_text() -> str:
|
|
121
|
+
lines = [
|
|
122
|
+
"## Proxy Configuration",
|
|
123
|
+
"",
|
|
124
|
+
"Configure proxy and SSL settings for HTTP requests.",
|
|
125
|
+
"",
|
|
126
|
+
"### Usage:",
|
|
127
|
+
"- `/proxy-setup` - Show this help and current settings",
|
|
128
|
+
"- `/proxy-setup KEY value` - Set an environment variable",
|
|
129
|
+
"- `/proxy-setup KEY` - Remove an environment variable",
|
|
130
|
+
"",
|
|
131
|
+
"### Supported Variables:",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
for key, description in SUPPORTED_PROXY_VARS.items():
|
|
135
|
+
lines.append(f"- `{key}`: {description}")
|
|
136
|
+
|
|
137
|
+
lines.extend(["", "### Current Settings:"])
|
|
138
|
+
|
|
139
|
+
current = get_current_proxy_settings()
|
|
140
|
+
any_set = False
|
|
141
|
+
for key, value in current.items():
|
|
142
|
+
if value:
|
|
143
|
+
lines.append(f"- `{key}={value}`")
|
|
144
|
+
any_set = True
|
|
145
|
+
|
|
146
|
+
if not any_set:
|
|
147
|
+
lines.append("- (none configured)")
|
|
148
|
+
|
|
149
|
+
return "\n".join(lines)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def create_user_message_replay(msg: LLMMessage) -> UserMessageChunk:
|
|
153
|
+
content = msg.content if isinstance(msg.content, str) else ""
|
|
154
|
+
return UserMessageChunk(
|
|
155
|
+
session_update="user_message_chunk",
|
|
156
|
+
content=TextContentBlock(type="text", text=content),
|
|
157
|
+
field_meta={"messageId": msg.message_id} if msg.message_id else {},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def create_assistant_message_replay(msg: LLMMessage) -> AgentMessageChunk | None:
|
|
162
|
+
content = msg.content if isinstance(msg.content, str) else ""
|
|
163
|
+
if not content:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
return AgentMessageChunk(
|
|
167
|
+
session_update="agent_message_chunk",
|
|
168
|
+
content=TextContentBlock(type="text", text=content),
|
|
169
|
+
field_meta={"messageId": msg.message_id} if msg.message_id else {},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def create_reasoning_replay(msg: LLMMessage) -> AgentThoughtChunk | None:
|
|
174
|
+
if not isinstance(msg.reasoning_content, str) or not msg.reasoning_content:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
return AgentThoughtChunk(
|
|
178
|
+
session_update="agent_thought_chunk",
|
|
179
|
+
content=TextContentBlock(type="text", text=msg.reasoning_content),
|
|
180
|
+
field_meta={"messageId": msg.message_id} if msg.message_id else {},
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def create_tool_call_replay(
|
|
185
|
+
tool_call_id: str, tool_name: str, arguments: str | None
|
|
186
|
+
) -> ToolCallStart:
|
|
187
|
+
return ToolCallStart(
|
|
188
|
+
session_update="tool_call",
|
|
189
|
+
title=tool_name,
|
|
190
|
+
tool_call_id=tool_call_id,
|
|
191
|
+
kind="other",
|
|
192
|
+
raw_input=arguments,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def create_tool_result_replay(msg: LLMMessage) -> ToolCallProgress | None:
|
|
197
|
+
if not msg.tool_call_id:
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
content = msg.content if isinstance(msg.content, str) else ""
|
|
201
|
+
return ToolCallProgress(
|
|
202
|
+
session_update="tool_call_update",
|
|
203
|
+
tool_call_id=msg.tool_call_id,
|
|
204
|
+
status="completed",
|
|
205
|
+
raw_output=content,
|
|
206
|
+
content=[
|
|
207
|
+
ContentToolCallContent(
|
|
208
|
+
type="content", content=TextContentBlock(type="text", text=content)
|
|
209
|
+
)
|
|
210
|
+
]
|
|
211
|
+
if content
|
|
212
|
+
else None,
|
|
213
|
+
)
|
vibe/cli/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CompletionResult(StrEnum):
|
|
8
|
+
IGNORED = "ignored"
|
|
9
|
+
HANDLED = "handled"
|
|
10
|
+
SUBMIT = "submit"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CompletionView(Protocol):
|
|
14
|
+
def render_completion_suggestions(
|
|
15
|
+
self, suggestions: list[tuple[str, str]], selected_index: int
|
|
16
|
+
) -> None: ...
|
|
17
|
+
|
|
18
|
+
def clear_completion_suggestions(self) -> None: ...
|
|
19
|
+
|
|
20
|
+
def replace_completion_range(
|
|
21
|
+
self, start: int, end: int, replacement: str
|
|
22
|
+
) -> None: ...
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
5
|
+
from threading import Lock
|
|
6
|
+
|
|
7
|
+
from textual import events
|
|
8
|
+
|
|
9
|
+
from vibe.cli.autocompletion.base import CompletionResult, CompletionView
|
|
10
|
+
from vibe.core.autocompletion.completers import PathCompleter
|
|
11
|
+
|
|
12
|
+
MAX_SUGGESTIONS_COUNT = 10
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PathCompletionController:
|
|
16
|
+
_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="path-completion")
|
|
17
|
+
|
|
18
|
+
def __init__(self, completer: PathCompleter, view: CompletionView) -> None:
|
|
19
|
+
self._completer = completer
|
|
20
|
+
self._view = view
|
|
21
|
+
self._suggestions: list[tuple[str, str]] = []
|
|
22
|
+
self._selected_index = 0
|
|
23
|
+
self._pending_future: Future | None = None
|
|
24
|
+
self._last_query: tuple[str, int] | None = None
|
|
25
|
+
self._query_lock = Lock()
|
|
26
|
+
|
|
27
|
+
def can_handle(self, text: str, cursor_index: int) -> bool:
|
|
28
|
+
if cursor_index < 0 or cursor_index > len(text):
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
if cursor_index == 0:
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
before_cursor = text[:cursor_index]
|
|
35
|
+
if "@" not in before_cursor:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
at_index = before_cursor.rfind("@")
|
|
39
|
+
|
|
40
|
+
if cursor_index <= at_index:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
fragment = before_cursor[at_index:cursor_index]
|
|
44
|
+
# fragment must not be empty (including @) and not contain any spaces
|
|
45
|
+
return bool(fragment) and " " not in fragment
|
|
46
|
+
|
|
47
|
+
def reset(self) -> None:
|
|
48
|
+
with self._query_lock:
|
|
49
|
+
if self._pending_future and not self._pending_future.done():
|
|
50
|
+
self._pending_future.cancel()
|
|
51
|
+
self._pending_future = None
|
|
52
|
+
self._last_query = None
|
|
53
|
+
if self._suggestions:
|
|
54
|
+
self._suggestions.clear()
|
|
55
|
+
self._selected_index = 0
|
|
56
|
+
self._view.clear_completion_suggestions()
|
|
57
|
+
|
|
58
|
+
def on_text_changed(self, text: str, cursor_index: int) -> None:
|
|
59
|
+
if not self.can_handle(text, cursor_index):
|
|
60
|
+
self.reset()
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
query = (text, cursor_index)
|
|
64
|
+
with self._query_lock:
|
|
65
|
+
if query == self._last_query:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
if self._pending_future and not self._pending_future.done():
|
|
69
|
+
# NOTE (Vince): this is a "best effort" cancellation: it only works if the task
|
|
70
|
+
# hasn't started; once running in the thread pool, it cannot be cancelled
|
|
71
|
+
self._pending_future.cancel()
|
|
72
|
+
|
|
73
|
+
self._last_query = query
|
|
74
|
+
|
|
75
|
+
app = getattr(self._view, "app", None)
|
|
76
|
+
if app:
|
|
77
|
+
with self._query_lock:
|
|
78
|
+
self._pending_future = self._executor.submit(
|
|
79
|
+
self._compute_completions, text, cursor_index
|
|
80
|
+
)
|
|
81
|
+
self._pending_future.add_done_callback(
|
|
82
|
+
lambda f: self._handle_completion_result(f, query)
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
suggestions = self._compute_completions(text, cursor_index)
|
|
86
|
+
self._update_suggestions(suggestions)
|
|
87
|
+
|
|
88
|
+
def _compute_completions(
|
|
89
|
+
self, text: str, cursor_index: int
|
|
90
|
+
) -> list[tuple[str, str]]:
|
|
91
|
+
return self._completer.get_completion_items(text, cursor_index)
|
|
92
|
+
|
|
93
|
+
def _handle_completion_result(self, future: Future, query: tuple[str, int]) -> None:
|
|
94
|
+
if future.cancelled():
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
suggestions = future.result()
|
|
99
|
+
with self._query_lock:
|
|
100
|
+
if query == self._last_query:
|
|
101
|
+
self._update_suggestions(suggestions)
|
|
102
|
+
except Exception:
|
|
103
|
+
with self._query_lock:
|
|
104
|
+
self._pending_future = None
|
|
105
|
+
self._last_query = None
|
|
106
|
+
|
|
107
|
+
def _update_suggestions(self, suggestions: list[tuple[str, str]]) -> None:
|
|
108
|
+
if len(suggestions) > MAX_SUGGESTIONS_COUNT:
|
|
109
|
+
suggestions = suggestions[:MAX_SUGGESTIONS_COUNT]
|
|
110
|
+
|
|
111
|
+
app = getattr(self._view, "app", None)
|
|
112
|
+
|
|
113
|
+
if suggestions:
|
|
114
|
+
self._suggestions = suggestions
|
|
115
|
+
self._selected_index = 0
|
|
116
|
+
if app:
|
|
117
|
+
app.call_after_refresh(
|
|
118
|
+
self._view.render_completion_suggestions,
|
|
119
|
+
self._suggestions,
|
|
120
|
+
self._selected_index,
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
self._view.render_completion_suggestions(
|
|
124
|
+
self._suggestions, self._selected_index
|
|
125
|
+
)
|
|
126
|
+
elif app:
|
|
127
|
+
app.call_after_refresh(self.reset)
|
|
128
|
+
else:
|
|
129
|
+
self.reset()
|
|
130
|
+
|
|
131
|
+
def on_key(
|
|
132
|
+
self, event: events.Key, text: str, cursor_index: int
|
|
133
|
+
) -> CompletionResult:
|
|
134
|
+
if not self._suggestions:
|
|
135
|
+
return CompletionResult.IGNORED
|
|
136
|
+
|
|
137
|
+
match event.key:
|
|
138
|
+
case "tab" | "enter":
|
|
139
|
+
if self._apply_selected_completion(text, cursor_index):
|
|
140
|
+
return CompletionResult.HANDLED
|
|
141
|
+
return CompletionResult.IGNORED
|
|
142
|
+
case "down":
|
|
143
|
+
self._move_selection(1)
|
|
144
|
+
return CompletionResult.HANDLED
|
|
145
|
+
case "up":
|
|
146
|
+
self._move_selection(-1)
|
|
147
|
+
return CompletionResult.HANDLED
|
|
148
|
+
case _:
|
|
149
|
+
return CompletionResult.IGNORED
|
|
150
|
+
|
|
151
|
+
def _move_selection(self, delta: int) -> None:
|
|
152
|
+
if not self._suggestions:
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
count = len(self._suggestions)
|
|
156
|
+
self._selected_index = (self._selected_index + delta) % count
|
|
157
|
+
self._view.render_completion_suggestions(
|
|
158
|
+
self._suggestions, self._selected_index
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _apply_selected_completion(self, text: str, cursor_index: int) -> bool:
|
|
162
|
+
if not self._suggestions:
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
completion, _ = self._suggestions[self._selected_index]
|
|
166
|
+
replacement_range = self._completer.get_replacement_range(text, cursor_index)
|
|
167
|
+
if replacement_range is None:
|
|
168
|
+
self.reset()
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
start, end = replacement_range
|
|
172
|
+
self._view.replace_completion_range(start, end, completion)
|
|
173
|
+
self.reset()
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
atexit.register(PathCompletionController._executor.shutdown)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from textual import events
|
|
4
|
+
|
|
5
|
+
from vibe.cli.autocompletion.base import CompletionResult, CompletionView
|
|
6
|
+
from vibe.core.autocompletion.completers import CommandCompleter
|
|
7
|
+
|
|
8
|
+
MAX_SUGGESTIONS_COUNT = 10
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SlashCommandController:
|
|
12
|
+
def __init__(self, completer: CommandCompleter, view: CompletionView) -> None:
|
|
13
|
+
self._completer = completer
|
|
14
|
+
self._view = view
|
|
15
|
+
self._suggestions: list[tuple[str, str]] = []
|
|
16
|
+
self._selected_index = 0
|
|
17
|
+
|
|
18
|
+
def can_handle(self, text: str, cursor_index: int) -> bool:
|
|
19
|
+
return text.startswith("/")
|
|
20
|
+
|
|
21
|
+
def reset(self) -> None:
|
|
22
|
+
if self._suggestions:
|
|
23
|
+
self._suggestions.clear()
|
|
24
|
+
self._selected_index = 0
|
|
25
|
+
self._view.clear_completion_suggestions()
|
|
26
|
+
|
|
27
|
+
def on_text_changed(self, text: str, cursor_index: int) -> None:
|
|
28
|
+
if cursor_index < 0 or cursor_index > len(text):
|
|
29
|
+
self.reset()
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
if not self.can_handle(text, cursor_index):
|
|
33
|
+
self.reset()
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
suggestions = self._completer.get_completion_items(text, cursor_index)
|
|
37
|
+
if len(suggestions) > MAX_SUGGESTIONS_COUNT:
|
|
38
|
+
suggestions = suggestions[:MAX_SUGGESTIONS_COUNT]
|
|
39
|
+
if suggestions:
|
|
40
|
+
self._suggestions = suggestions
|
|
41
|
+
self._selected_index = 0
|
|
42
|
+
self._view.render_completion_suggestions(
|
|
43
|
+
self._suggestions, self._selected_index
|
|
44
|
+
)
|
|
45
|
+
else:
|
|
46
|
+
self.reset()
|
|
47
|
+
|
|
48
|
+
def on_key(
|
|
49
|
+
self, event: events.Key, text: str, cursor_index: int
|
|
50
|
+
) -> CompletionResult:
|
|
51
|
+
if not self._suggestions:
|
|
52
|
+
return CompletionResult.IGNORED
|
|
53
|
+
|
|
54
|
+
match event.key:
|
|
55
|
+
case "tab":
|
|
56
|
+
if self._apply_selected_completion(text, cursor_index):
|
|
57
|
+
result = CompletionResult.HANDLED
|
|
58
|
+
else:
|
|
59
|
+
result = CompletionResult.IGNORED
|
|
60
|
+
case "enter":
|
|
61
|
+
if self._apply_selected_completion(text, cursor_index):
|
|
62
|
+
result = CompletionResult.SUBMIT
|
|
63
|
+
else:
|
|
64
|
+
result = CompletionResult.HANDLED
|
|
65
|
+
case "down":
|
|
66
|
+
self._move_selection(1)
|
|
67
|
+
result = CompletionResult.HANDLED
|
|
68
|
+
case "up":
|
|
69
|
+
self._move_selection(-1)
|
|
70
|
+
result = CompletionResult.HANDLED
|
|
71
|
+
case _:
|
|
72
|
+
result = CompletionResult.IGNORED
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
def _move_selection(self, delta: int) -> None:
|
|
77
|
+
if not self._suggestions:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
count = len(self._suggestions)
|
|
81
|
+
self._selected_index = (self._selected_index + delta) % count
|
|
82
|
+
self._view.render_completion_suggestions(
|
|
83
|
+
self._suggestions, self._selected_index
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def _apply_selected_completion(self, text: str, cursor_index: int) -> bool:
|
|
87
|
+
if not self._suggestions:
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
alias, _ = self._suggestions[self._selected_index]
|
|
91
|
+
replacement_range = self._completer.get_replacement_range(text, cursor_index)
|
|
92
|
+
if replacement_range is None:
|
|
93
|
+
self.reset()
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
start, end = replacement_range
|
|
97
|
+
self._view.replace_completion_range(start, end, alias)
|
|
98
|
+
self.reset()
|
|
99
|
+
return True
|