v2.7.3 (#564)
Co-authored-by: Bastien <bastien.baret@gmail.com> Co-authored-by: Laure Hugo <201583486+laure0303@users.noreply.github.com> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Paul Cacheux <paul.cacheux@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
9c1c32e058
commit
90763daf81
61 changed files with 6046 additions and 694 deletions
|
|
@ -9,7 +9,7 @@ import tomli_w
|
|||
|
||||
from vibe import __version__
|
||||
from vibe.cli.textual_ui.app import StartupOptions, run_textual_ui
|
||||
from vibe.core.agent_loop import AgentLoop
|
||||
from vibe.core.agent_loop import AgentLoop, TeleportError
|
||||
from vibe.core.agents.models import BuiltinAgentName
|
||||
from vibe.core.config import (
|
||||
MissingAPIKeyError,
|
||||
|
|
@ -176,12 +176,13 @@ def run_cli(args: argparse.Namespace) -> None:
|
|||
try:
|
||||
final_response = run_programmatic(
|
||||
config=config,
|
||||
prompt=programmatic_prompt,
|
||||
prompt=programmatic_prompt or "",
|
||||
max_turns=args.max_turns,
|
||||
max_price=args.max_price,
|
||||
output_format=output_format,
|
||||
previous_messages=loaded_session[0] if loaded_session else None,
|
||||
agent_name=initial_agent_name,
|
||||
teleport=args.teleport and config.nuage_enabled,
|
||||
)
|
||||
if final_response:
|
||||
print(final_response)
|
||||
|
|
@ -189,6 +190,9 @@ def run_cli(args: argparse.Namespace) -> None:
|
|||
except ConversationLimitException as e:
|
||||
print(e, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except TeleportError as e:
|
||||
print(f"Teleport error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except RuntimeError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
|
|
|||
|
|
@ -105,6 +105,11 @@ class CommandRegistry:
|
|||
description="Rewind to a previous message",
|
||||
handler="_start_rewind_mode",
|
||||
),
|
||||
"data-retention": Command(
|
||||
aliases=frozenset(["/data-retention"]),
|
||||
description="Show data retention information",
|
||||
handler="_show_data_retention",
|
||||
),
|
||||
}
|
||||
|
||||
for command in excluded_commands:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import subprocess
|
|||
import time
|
||||
from typing import Any, ClassVar, assert_never, cast
|
||||
from weakref import WeakKeyDictionary
|
||||
import webbrowser
|
||||
|
||||
from pydantic import BaseModel
|
||||
from rich import print as rprint
|
||||
|
|
@ -46,6 +47,7 @@ from vibe.cli.textual_ui.notifications import (
|
|||
NotificationPort,
|
||||
TextualNotificationAdapter,
|
||||
)
|
||||
from vibe.cli.textual_ui.remote import RemoteSessionManager, is_progress_event
|
||||
from vibe.cli.textual_ui.session_exit import print_session_resume_message
|
||||
from vibe.cli.textual_ui.widgets.approval_app import ApprovalApp
|
||||
from vibe.cli.textual_ui.widgets.banner.banner import Banner
|
||||
|
|
@ -109,20 +111,28 @@ from vibe.core.audio_player.audio_player import AudioPlayer
|
|||
from vibe.core.audio_recorder import AudioRecorder
|
||||
from vibe.core.autocompletion.path_prompt_adapter import render_path_prompt
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.core.data_retention import DATA_RETENTION_MESSAGE
|
||||
from vibe.core.logger import logger
|
||||
from vibe.core.paths import HISTORY_FILE
|
||||
from vibe.core.rewind import RewindError
|
||||
from vibe.core.session.resume_sessions import (
|
||||
ResumeSessionInfo,
|
||||
list_local_resume_sessions,
|
||||
list_remote_resume_sessions,
|
||||
short_session_id,
|
||||
)
|
||||
from vibe.core.session.session_loader import SessionLoader
|
||||
from vibe.core.teleport.types import (
|
||||
TeleportAuthCompleteEvent,
|
||||
TeleportAuthRequiredEvent,
|
||||
TeleportCheckingGitEvent,
|
||||
TeleportCompleteEvent,
|
||||
TeleportFetchingUrlEvent,
|
||||
TeleportPushingEvent,
|
||||
TeleportPushRequiredEvent,
|
||||
TeleportPushResponseEvent,
|
||||
TeleportSendingGithubTokenEvent,
|
||||
TeleportStartingWorkflowEvent,
|
||||
TeleportWaitingForGitHubEvent,
|
||||
)
|
||||
from vibe.core.tools.builtins.ask_user_question import (
|
||||
AskUserQuestionArgs,
|
||||
|
|
@ -136,9 +146,11 @@ from vibe.core.types import (
|
|||
AgentStats,
|
||||
ApprovalResponse,
|
||||
Backend,
|
||||
BaseEvent,
|
||||
LLMMessage,
|
||||
RateLimitError,
|
||||
Role,
|
||||
WaitingForInputEvent,
|
||||
)
|
||||
from vibe.core.utils import (
|
||||
CancellationReason,
|
||||
|
|
@ -302,6 +314,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self._agent_running = False
|
||||
self._interrupt_requested = False
|
||||
self._agent_task: asyncio.Task | None = None
|
||||
self._remote_manager = RemoteSessionManager()
|
||||
|
||||
self._loading_widget: LoadingWidget | None = None
|
||||
self._pending_approval: asyncio.Future | None = None
|
||||
|
|
@ -398,6 +411,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
mount_callback=self._mount_and_scroll,
|
||||
get_tools_collapsed=lambda: self._tools_collapsed,
|
||||
on_profile_changed=self._on_profile_changed,
|
||||
is_remote=self._remote_manager.is_active,
|
||||
)
|
||||
|
||||
self._chat_input_container = self.query_one(ChatInputContainer)
|
||||
|
|
@ -512,11 +526,21 @@ class VibeApp(App): # noqa: PLR0904
|
|||
await self._remove_loading_widget()
|
||||
|
||||
async def on_question_app_answered(self, message: QuestionApp.Answered) -> None:
|
||||
if self._remote_manager.has_pending_input and self._remote_manager.is_active:
|
||||
result = AskUserQuestionResult(answers=message.answers, cancelled=False)
|
||||
await self._handle_remote_answer(result)
|
||||
return
|
||||
|
||||
if self._pending_question and not self._pending_question.done():
|
||||
result = AskUserQuestionResult(answers=message.answers, cancelled=False)
|
||||
self._pending_question.set_result(result)
|
||||
|
||||
async def on_question_app_cancelled(self, message: QuestionApp.Cancelled) -> None:
|
||||
if self._remote_manager.has_pending_input:
|
||||
self._remote_manager.cancel_pending_input()
|
||||
await self._switch_to_input_app()
|
||||
return
|
||||
|
||||
if self._pending_question and not self._pending_question.done():
|
||||
result = AskUserQuestionResult(answers=[], cancelled=True)
|
||||
self._pending_question.set_result(result)
|
||||
|
|
@ -554,6 +578,21 @@ class VibeApp(App): # noqa: PLR0904
|
|||
await self._switch_to_input_app()
|
||||
await self._switch_to_model_picker_app()
|
||||
|
||||
async def _ensure_loading_widget(self, status: str = "Generating") -> None:
|
||||
if self._loading_widget and self._loading_widget.parent:
|
||||
self._loading_widget.set_status(status)
|
||||
return
|
||||
|
||||
loading_area = self._cached_loading_area
|
||||
if loading_area is None:
|
||||
try:
|
||||
loading_area = self.query_one("#loading-area-content")
|
||||
except Exception:
|
||||
return
|
||||
loading = LoadingWidget(status=status)
|
||||
self._loading_widget = loading
|
||||
await loading_area.mount(loading)
|
||||
|
||||
async def on_config_app_config_closed(
|
||||
self, message: ConfigApp.ConfigClosed
|
||||
) -> None:
|
||||
|
|
@ -755,6 +794,10 @@ class VibeApp(App): # noqa: PLR0904
|
|||
)
|
||||
|
||||
async def _handle_user_message(self, message: str) -> None:
|
||||
if self._remote_manager.is_active:
|
||||
await self._handle_remote_user_message(message)
|
||||
return
|
||||
|
||||
# message_index is where the user message will land in agent_loop.messages
|
||||
# (checkpoint is created in agent_loop.act())
|
||||
message_index = len(self.agent_loop.messages)
|
||||
|
|
@ -765,10 +808,46 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self._feedback_bar.maybe_show()
|
||||
|
||||
if not self._agent_running:
|
||||
await self._remote_manager.stop_stream()
|
||||
await self._remove_loading_widget()
|
||||
self._agent_task = asyncio.create_task(
|
||||
self._handle_agent_loop_turn(message)
|
||||
)
|
||||
|
||||
async def _handle_remote_user_message(self, message: str) -> None:
|
||||
warning = self._remote_manager.validate_input()
|
||||
if warning:
|
||||
await self._mount_and_scroll(WarningMessage(warning))
|
||||
return
|
||||
try:
|
||||
await self._remote_manager.send_prompt(message)
|
||||
except Exception as e:
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
f"Failed to send message: {e}", collapsed=self._tools_collapsed
|
||||
)
|
||||
)
|
||||
return
|
||||
await self._ensure_loading_widget()
|
||||
|
||||
async def _handle_remote_waiting_input(self, event: WaitingForInputEvent) -> None:
|
||||
self._remote_manager.set_pending_input(event)
|
||||
if question_args := self._remote_manager.build_question_args(event):
|
||||
await self._switch_to_question_app(question_args)
|
||||
return
|
||||
await self._switch_to_input_app()
|
||||
|
||||
async def _handle_remote_answer(self, result: AskUserQuestionResult) -> None:
|
||||
if result.cancelled or not result.answers:
|
||||
self._remote_manager.cancel_pending_input()
|
||||
await self._switch_to_input_app()
|
||||
return
|
||||
await self._remote_manager.send_prompt(
|
||||
result.answers[0].answer, require_source=False
|
||||
)
|
||||
await self._switch_to_input_app()
|
||||
await self._ensure_loading_widget()
|
||||
|
||||
def _reset_ui_state(self) -> None:
|
||||
self._windowing.reset()
|
||||
self._tool_call_map = None
|
||||
|
|
@ -881,13 +960,8 @@ class VibeApp(App): # noqa: PLR0904
|
|||
async def _handle_agent_loop_turn(self, prompt: str) -> None:
|
||||
self._agent_running = True
|
||||
|
||||
loading_area = self._cached_loading_area or self.query_one(
|
||||
"#loading-area-content"
|
||||
)
|
||||
|
||||
loading = LoadingWidget()
|
||||
self._loading_widget = loading
|
||||
await loading_area.mount(loading)
|
||||
await self._remove_loading_widget()
|
||||
await self._ensure_loading_widget()
|
||||
|
||||
try:
|
||||
rendered_prompt = render_path_prompt(prompt, base_dir=Path.cwd())
|
||||
|
|
@ -895,6 +969,12 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self._narrator_manager.on_turn_start(rendered_prompt)
|
||||
async for event in self.agent_loop.act(rendered_prompt):
|
||||
self._narrator_manager.on_turn_event(event)
|
||||
if isinstance(event, WaitingForInputEvent):
|
||||
await self._remove_loading_widget()
|
||||
if self._remote_manager.is_active:
|
||||
await self._handle_remote_waiting_input(event)
|
||||
elif self._loading_widget is None and is_progress_event(event):
|
||||
await self._ensure_loading_widget()
|
||||
if self.event_handler:
|
||||
await self.event_handler.handle_event(
|
||||
event,
|
||||
|
|
@ -972,32 +1052,47 @@ class VibeApp(App): # noqa: PLR0904
|
|||
teleport_msg = TeleportMessage()
|
||||
await self._mount_and_scroll(teleport_msg)
|
||||
|
||||
if self._remote_manager.is_active:
|
||||
await loading.remove()
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
"Teleport is not available for remote sessions.",
|
||||
collapsed=self._tools_collapsed,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
gen = self.agent_loop.teleport_to_vibe_nuage(prompt)
|
||||
async for event in gen:
|
||||
match event:
|
||||
case TeleportCheckingGitEvent():
|
||||
teleport_msg.set_status("Checking git status...")
|
||||
case TeleportPushRequiredEvent(unpushed_count=count):
|
||||
teleport_msg.set_status("Preparing workspace...")
|
||||
case TeleportPushRequiredEvent(
|
||||
unpushed_count=count, branch_not_pushed=branch_not_pushed
|
||||
):
|
||||
await loading.remove()
|
||||
response = await self._ask_push_approval(count)
|
||||
response = await self._ask_push_approval(
|
||||
count, branch_not_pushed
|
||||
)
|
||||
await loading_area.mount(loading)
|
||||
teleport_msg.set_status("Teleporting...")
|
||||
await gen.asend(response)
|
||||
next_event = await gen.asend(response)
|
||||
if isinstance(next_event, TeleportPushingEvent):
|
||||
teleport_msg.set_status("Syncing with remote...")
|
||||
case TeleportPushingEvent():
|
||||
teleport_msg.set_status("Pushing to remote...")
|
||||
case TeleportAuthRequiredEvent(
|
||||
user_code=code, verification_uri=uri
|
||||
):
|
||||
teleport_msg.set_status(
|
||||
f"GitHub auth required. Code: {code} (copied)\nOpen: {uri}"
|
||||
)
|
||||
case TeleportAuthCompleteEvent():
|
||||
teleport_msg.set_status("GitHub authenticated.")
|
||||
teleport_msg.set_status("Syncing with remote...")
|
||||
case TeleportStartingWorkflowEvent():
|
||||
teleport_msg.set_status("Starting Nuage workflow...")
|
||||
case TeleportSendingGithubTokenEvent():
|
||||
teleport_msg.set_status("Sending encrypted GitHub token...")
|
||||
teleport_msg.set_status("Teleporting...")
|
||||
case TeleportWaitingForGitHubEvent():
|
||||
teleport_msg.set_status("Connecting to GitHub...")
|
||||
case TeleportAuthRequiredEvent(oauth_url=url):
|
||||
webbrowser.open(url)
|
||||
teleport_msg.set_status("Authorizing GitHub...")
|
||||
case TeleportAuthCompleteEvent():
|
||||
teleport_msg.set_status("GitHub authorized")
|
||||
case TeleportFetchingUrlEvent():
|
||||
teleport_msg.set_status("Finalizing...")
|
||||
case TeleportCompleteEvent(url=url):
|
||||
teleport_msg.set_complete(url)
|
||||
except TeleportError as e:
|
||||
|
|
@ -1009,14 +1104,20 @@ class VibeApp(App): # noqa: PLR0904
|
|||
if loading.parent:
|
||||
await loading.remove()
|
||||
|
||||
async def _ask_push_approval(self, count: int) -> TeleportPushResponseEvent:
|
||||
word = f"commit{'s' if count != 1 else ''}"
|
||||
async def _ask_push_approval(
|
||||
self, count: int, branch_not_pushed: bool
|
||||
) -> TeleportPushResponseEvent:
|
||||
if branch_not_pushed:
|
||||
question = "Your branch doesn't exist on remote. Push to continue?"
|
||||
else:
|
||||
word = f"commit{'s' if count != 1 else ''}"
|
||||
question = f"You have {count} unpushed {word}. Push to continue?"
|
||||
push_label = "Push and continue"
|
||||
result = await self._user_input_callback(
|
||||
AskUserQuestionArgs(
|
||||
questions=[
|
||||
Question(
|
||||
question=f"You have {count} unpushed {word}. Push to continue?",
|
||||
question=question,
|
||||
header="Push",
|
||||
options=[Choice(label=push_label), Choice(label="Cancel")],
|
||||
hide_other=True,
|
||||
|
|
@ -1105,20 +1206,41 @@ class VibeApp(App): # noqa: PLR0904
|
|||
return
|
||||
await self._switch_to_proxy_setup_app()
|
||||
|
||||
async def _show_data_retention(self) -> None:
|
||||
await self._mount_and_scroll(UserCommandMessage(DATA_RETENTION_MESSAGE))
|
||||
|
||||
async def _show_session_picker(self) -> None:
|
||||
session_config = self.config.session_logging
|
||||
|
||||
if not session_config.enabled:
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
"Session logging is disabled in configuration.",
|
||||
collapsed=self._tools_collapsed,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
cwd = str(Path.cwd())
|
||||
raw_sessions = SessionLoader.list_sessions(session_config, cwd=cwd)
|
||||
local_sessions = (
|
||||
list_local_resume_sessions(self.config, cwd)
|
||||
if self.config.session_logging.enabled
|
||||
else []
|
||||
)
|
||||
remote_list_timeout = max(float(self.config.api_timeout), 10.0)
|
||||
remote_error: str | None = None
|
||||
await self._ensure_loading_widget("Loading sessions")
|
||||
try:
|
||||
remote_sessions = await asyncio.wait_for(
|
||||
list_remote_resume_sessions(self.config), timeout=remote_list_timeout
|
||||
)
|
||||
except TimeoutError:
|
||||
remote_sessions = []
|
||||
remote_error = (
|
||||
"Timed out while listing remote sessions "
|
||||
f"after {remote_list_timeout:.0f}s."
|
||||
)
|
||||
except Exception as e:
|
||||
remote_sessions = []
|
||||
remote_error = f"Failed to list remote sessions: {e}"
|
||||
finally:
|
||||
await self._remove_loading_widget()
|
||||
|
||||
if remote_error is not None:
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(remote_error, collapsed=self._tools_collapsed)
|
||||
)
|
||||
|
||||
raw_sessions = [*local_sessions, *remote_sessions]
|
||||
|
||||
if not raw_sessions:
|
||||
await self._mount_and_scroll(
|
||||
|
|
@ -1126,16 +1248,20 @@ class VibeApp(App): # noqa: PLR0904
|
|||
)
|
||||
return
|
||||
|
||||
sessions = sorted(
|
||||
raw_sessions, key=lambda s: s.get("end_time") or "", reverse=True
|
||||
)
|
||||
sessions = sorted(raw_sessions, key=lambda s: s.end_time or "", reverse=True)
|
||||
|
||||
latest_messages = {
|
||||
s["session_id"]: SessionLoader.get_first_user_message(
|
||||
s["session_id"], session_config
|
||||
s.option_id: SessionLoader.get_first_user_message(
|
||||
s.session_id, self.config.session_logging
|
||||
)
|
||||
for s in sessions
|
||||
if s.source == "local"
|
||||
}
|
||||
for session in sessions:
|
||||
if session.source == "remote":
|
||||
latest_messages[session.option_id] = (
|
||||
f"{session.title or 'Remote workflow'} ({(session.status or 'RUNNING').lower()})"
|
||||
)
|
||||
|
||||
picker = SessionPickerApp(sessions=sessions, latest_messages=latest_messages)
|
||||
await self._switch_from_input(picker)
|
||||
|
|
@ -1144,53 +1270,21 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self, event: SessionPickerApp.SessionSelected
|
||||
) -> None:
|
||||
await self._switch_to_input_app()
|
||||
|
||||
session_config = self.config.session_logging
|
||||
session_path = SessionLoader.find_session_by_id(
|
||||
event.session_id, session_config
|
||||
session = ResumeSessionInfo(
|
||||
session_id=event.session_id,
|
||||
source=event.source,
|
||||
cwd="",
|
||||
title=None,
|
||||
end_time=None,
|
||||
)
|
||||
|
||||
if not session_path:
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
f"Session `{event.session_id[:8]}` not found.",
|
||||
collapsed=self._tools_collapsed,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
loaded_messages, _ = SessionLoader.load_session(session_path)
|
||||
|
||||
current_system_messages = [
|
||||
msg for msg in self.agent_loop.messages if msg.role == Role.system
|
||||
]
|
||||
non_system_messages = [
|
||||
msg for msg in loaded_messages if msg.role != Role.system
|
||||
]
|
||||
|
||||
self.agent_loop.session_id = event.session_id
|
||||
self.agent_loop.session_logger.resume_existing_session(
|
||||
event.session_id, session_path
|
||||
)
|
||||
|
||||
self.agent_loop.messages.reset(
|
||||
current_system_messages + non_system_messages
|
||||
)
|
||||
|
||||
self._reset_ui_state()
|
||||
await self._load_more.hide()
|
||||
|
||||
messages_area = self._cached_messages_area or self.query_one("#messages")
|
||||
await messages_area.remove_children()
|
||||
|
||||
await self._resume_history_from_messages()
|
||||
|
||||
await self._mount_and_scroll(
|
||||
UserCommandMessage(f"Resumed session `{event.session_id[:8]}`")
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
if event.source == "local":
|
||||
await self._resume_local_session(session)
|
||||
elif event.source == "remote":
|
||||
await self._resume_remote_session(session)
|
||||
else:
|
||||
raise ValueError(f"Unknown session source: {event.source}")
|
||||
except Exception as e:
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
f"Failed to load session: {e}", collapsed=self._tools_collapsed
|
||||
|
|
@ -1204,6 +1298,113 @@ class VibeApp(App): # noqa: PLR0904
|
|||
|
||||
await self._mount_and_scroll(UserCommandMessage("Resume cancelled."))
|
||||
|
||||
async def _resume_local_session(self, session: ResumeSessionInfo) -> None:
|
||||
await self._remote_manager.detach()
|
||||
session_config = self.config.session_logging
|
||||
session_path = SessionLoader.find_session_by_id(
|
||||
session.session_id, session_config
|
||||
)
|
||||
|
||||
if not session_path:
|
||||
raise ValueError(
|
||||
f"Session `{short_session_id(session.session_id)}` not found."
|
||||
)
|
||||
|
||||
loaded_messages, _ = SessionLoader.load_session(session_path)
|
||||
if self._chat_input_container:
|
||||
self._chat_input_container.set_custom_border(None)
|
||||
|
||||
current_system_messages = [
|
||||
msg for msg in self.agent_loop.messages if msg.role == Role.system
|
||||
]
|
||||
non_system_messages = [
|
||||
msg for msg in loaded_messages if msg.role != Role.system
|
||||
]
|
||||
|
||||
self.agent_loop.session_id = session.session_id
|
||||
self.agent_loop.session_logger.resume_existing_session(
|
||||
session.session_id, session_path
|
||||
)
|
||||
self.agent_loop.messages.reset(current_system_messages + non_system_messages)
|
||||
self._refresh_profile_widgets()
|
||||
|
||||
self._reset_ui_state()
|
||||
await self._load_more.hide()
|
||||
|
||||
messages_area = self._cached_messages_area or self.query_one("#messages")
|
||||
await messages_area.remove_children()
|
||||
|
||||
if self.event_handler:
|
||||
self.event_handler.is_remote = False
|
||||
await self._resume_history_from_messages()
|
||||
await self._mount_and_scroll(
|
||||
UserCommandMessage(
|
||||
f"Resumed session `{short_session_id(session.session_id)}`"
|
||||
)
|
||||
)
|
||||
|
||||
async def _resume_remote_session(self, session: ResumeSessionInfo) -> None:
|
||||
await self._remote_manager.attach(
|
||||
session_id=session.session_id, config=self.config
|
||||
)
|
||||
self._refresh_profile_widgets()
|
||||
if self._chat_input_container:
|
||||
self._chat_input_container.set_custom_border(None)
|
||||
|
||||
self._reset_ui_state()
|
||||
await self._load_more.hide()
|
||||
|
||||
messages_area = self._cached_messages_area or self.query_one("#messages")
|
||||
await messages_area.remove_children()
|
||||
|
||||
if self.event_handler:
|
||||
self.event_handler.is_remote = True
|
||||
self._remote_manager.start_stream(self)
|
||||
|
||||
async def on_remote_event(
|
||||
self, event: BaseEvent, loading_active: bool, loading_widget: Any
|
||||
) -> None:
|
||||
if self.event_handler:
|
||||
await self.event_handler.handle_event(
|
||||
event, loading_active=loading_active, loading_widget=loading_widget
|
||||
)
|
||||
|
||||
async def on_remote_waiting_input(self, event: WaitingForInputEvent) -> None:
|
||||
await self._handle_remote_waiting_input(event)
|
||||
|
||||
async def on_remote_user_message_cleared_input(self) -> None:
|
||||
await self._switch_to_input_app()
|
||||
|
||||
async def on_remote_stream_error(self, error: str) -> None:
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(error, collapsed=self._tools_collapsed)
|
||||
)
|
||||
|
||||
async def on_remote_stream_ended(self, msg_type: str, text: str) -> None:
|
||||
if msg_type == "error":
|
||||
widget = ErrorMessage(text, collapsed=self._tools_collapsed)
|
||||
elif msg_type == "warning":
|
||||
widget = WarningMessage(text)
|
||||
else:
|
||||
widget = UserCommandMessage(text)
|
||||
await self._mount_and_scroll(widget)
|
||||
if self._chat_input_container:
|
||||
self._chat_input_container.set_custom_border("Remote session ended")
|
||||
|
||||
async def on_remote_finalize_streaming(self) -> None:
|
||||
if self.event_handler:
|
||||
await self.event_handler.finalize_streaming()
|
||||
|
||||
async def remove_loading(self) -> None:
|
||||
await self._remove_loading_widget()
|
||||
|
||||
async def ensure_loading(self, status: str = "Generating") -> None:
|
||||
await self._ensure_loading_widget(status)
|
||||
|
||||
@property
|
||||
def loading_widget(self) -> LoadingWidget | None:
|
||||
return self._loading_widget
|
||||
|
||||
async def _reload_config(self) -> None:
|
||||
try:
|
||||
self._reset_ui_state()
|
||||
|
|
@ -1254,6 +1455,13 @@ class VibeApp(App): # noqa: PLR0904
|
|||
async def _clear_history(self) -> None:
|
||||
try:
|
||||
self._reset_ui_state()
|
||||
if self._remote_manager.is_active:
|
||||
await self._remote_manager.detach()
|
||||
self._refresh_profile_widgets()
|
||||
if self.event_handler:
|
||||
self.event_handler.is_remote = False
|
||||
if self._chat_input_container:
|
||||
self._chat_input_container.set_custom_border(None)
|
||||
await self.agent_loop.clear_history()
|
||||
if self.event_handler:
|
||||
await self.event_handler.finalize_streaming()
|
||||
|
|
@ -1348,6 +1556,8 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self.event_handler.current_compact = None
|
||||
|
||||
def _get_session_resume_info(self) -> str | None:
|
||||
if self._remote_manager.is_active:
|
||||
return None
|
||||
if not self.agent_loop.session_logger.enabled:
|
||||
return None
|
||||
if not self.agent_loop.session_logger.session_id:
|
||||
|
|
@ -1358,7 +1568,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
)
|
||||
if session_path is None:
|
||||
return None
|
||||
return self.agent_loop.session_logger.session_id[:8]
|
||||
return short_session_id(self.agent_loop.session_logger.session_id)
|
||||
|
||||
async def _exit_app(self) -> None:
|
||||
await self._narrator_manager.close()
|
||||
|
|
@ -1918,6 +2128,14 @@ class VibeApp(App): # noqa: PLR0904
|
|||
if self._chat_input_container:
|
||||
self._chat_input_container.set_safety(profile.safety)
|
||||
self._chat_input_container.set_agent_name(profile.display_name.lower())
|
||||
if self._remote_manager.is_active:
|
||||
session_id = self._remote_manager.session_id
|
||||
self._chat_input_container.set_custom_border(
|
||||
f"Remote session {short_session_id(session_id, source='remote') if session_id else ''}",
|
||||
ChatInputContainer.REMOTE_BORDER_CLASS,
|
||||
)
|
||||
else:
|
||||
self._chat_input_container.set_custom_border(None)
|
||||
|
||||
async def _cycle_agent(self) -> None:
|
||||
new_profile = self.agent_loop.agent_manager.next_agent(
|
||||
|
|
@ -1965,6 +2183,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
def action_force_quit(self) -> None:
|
||||
if self._agent_task and not self._agent_task.done():
|
||||
self._agent_task.cancel()
|
||||
self._remote_manager.cancel_stream_task()
|
||||
|
||||
self._narrator_manager.cancel()
|
||||
self.exit(result=self._get_session_resume_info())
|
||||
|
|
|
|||
|
|
@ -118,6 +118,11 @@ TextArea > .text-area--cursor {
|
|||
border: solid $mistral_orange;
|
||||
border-title-color: $mistral_orange;
|
||||
}
|
||||
|
||||
&.border-remote {
|
||||
border: solid $mistral_orange;
|
||||
border-title-color: $mistral_orange;
|
||||
}
|
||||
}
|
||||
|
||||
#input-body {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@ from collections.abc import Callable
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from vibe.cli.textual_ui.widgets.compact import CompactMessage
|
||||
from vibe.cli.textual_ui.widgets.messages import AssistantMessage, ReasoningMessage
|
||||
from vibe.cli.textual_ui.widgets.messages import (
|
||||
AssistantMessage,
|
||||
ReasoningMessage,
|
||||
UserMessage,
|
||||
)
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.tools import ToolCallMessage, ToolResultMessage
|
||||
from vibe.core.tools.ui import ToolUIDataAdapter
|
||||
|
|
@ -19,6 +23,7 @@ from vibe.core.types import (
|
|||
ToolResultEvent,
|
||||
ToolStreamEvent,
|
||||
UserMessageEvent,
|
||||
WaitingForInputEvent,
|
||||
)
|
||||
from vibe.core.utils import TaggedText
|
||||
|
||||
|
|
@ -32,10 +37,12 @@ class EventHandler:
|
|||
mount_callback: Callable,
|
||||
get_tools_collapsed: Callable[[], bool],
|
||||
on_profile_changed: Callable[[], None] | None = None,
|
||||
is_remote: bool = False,
|
||||
) -> None:
|
||||
self.mount_callback = mount_callback
|
||||
self.get_tools_collapsed = get_tools_collapsed
|
||||
self.on_profile_changed = on_profile_changed
|
||||
self.is_remote = is_remote
|
||||
self.tool_calls: dict[str, ToolCallMessage] = {}
|
||||
self.current_compact: CompactMessage | None = None
|
||||
self.current_streaming_message: AssistantMessage | None = None
|
||||
|
|
@ -72,6 +79,10 @@ class EventHandler:
|
|||
self.on_profile_changed()
|
||||
case UserMessageEvent():
|
||||
await self.finalize_streaming()
|
||||
if self.is_remote:
|
||||
await self.mount_callback(UserMessage(event.content))
|
||||
case WaitingForInputEvent():
|
||||
await self.finalize_streaming()
|
||||
case _:
|
||||
await self.finalize_streaming()
|
||||
await self._handle_unknown_event(event)
|
||||
|
|
|
|||
8
vibe/cli/textual_ui/remote/__init__.py
Normal file
8
vibe/cli/textual_ui/remote/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.cli.textual_ui.remote.remote_session_manager import (
|
||||
RemoteSessionManager,
|
||||
is_progress_event,
|
||||
)
|
||||
|
||||
__all__ = ["RemoteSessionManager", "is_progress_event"]
|
||||
216
vibe/cli/textual_ui/remote/remote_session_manager.py
Normal file
216
vibe/cli/textual_ui/remote/remote_session_manager.py
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any, Protocol
|
||||
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.core.nuage.remote_events_source import RemoteEventsSource
|
||||
from vibe.core.tools.builtins.ask_user_question import (
|
||||
AskUserQuestionArgs,
|
||||
Choice,
|
||||
Question,
|
||||
)
|
||||
from vibe.core.types import (
|
||||
AssistantEvent,
|
||||
BaseEvent,
|
||||
ReasoningEvent,
|
||||
ToolCallEvent,
|
||||
ToolStreamEvent,
|
||||
UserMessageEvent,
|
||||
WaitingForInputEvent,
|
||||
)
|
||||
|
||||
_MIN_QUESTION_OPTIONS = 2
|
||||
_MAX_QUESTION_OPTIONS = 4
|
||||
|
||||
|
||||
class RemoteSessionUI(Protocol):
|
||||
async def on_remote_event(
|
||||
self, event: BaseEvent, loading_active: bool, loading_widget: Any
|
||||
) -> None: ...
|
||||
async def on_remote_waiting_input(self, event: WaitingForInputEvent) -> None: ...
|
||||
async def on_remote_user_message_cleared_input(self) -> None: ...
|
||||
async def on_remote_stream_error(self, error: str) -> None: ...
|
||||
async def on_remote_stream_ended(self, msg_type: str, text: str) -> None: ...
|
||||
async def on_remote_finalize_streaming(self) -> None: ...
|
||||
async def remove_loading(self) -> None: ...
|
||||
async def ensure_loading(self, status: str = "Generating") -> None: ...
|
||||
@property
|
||||
def loading_widget(self) -> Any: ...
|
||||
|
||||
|
||||
def is_progress_event(event: object) -> bool:
|
||||
return isinstance(
|
||||
event, (AssistantEvent, ReasoningEvent, ToolCallEvent, ToolStreamEvent)
|
||||
)
|
||||
|
||||
|
||||
class RemoteSessionManager:
|
||||
def __init__(self) -> None:
|
||||
self._events_source: RemoteEventsSource | None = None
|
||||
self._stream_task: asyncio.Task | None = None
|
||||
self._pending_waiting_input: WaitingForInputEvent | None = None
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
return self._events_source is not None
|
||||
|
||||
@property
|
||||
def is_terminated(self) -> bool:
|
||||
if self._events_source is None:
|
||||
return False
|
||||
return self._events_source.is_terminated
|
||||
|
||||
@property
|
||||
def is_waiting_for_input(self) -> bool:
|
||||
if self._events_source is None:
|
||||
return False
|
||||
return self._events_source.is_waiting_for_input
|
||||
|
||||
@property
|
||||
def has_pending_input(self) -> bool:
|
||||
return self._pending_waiting_input is not None
|
||||
|
||||
@property
|
||||
def session_id(self) -> str | None:
|
||||
if self._events_source is None:
|
||||
return None
|
||||
return self._events_source.session_id
|
||||
|
||||
async def attach(self, session_id: str, config: VibeConfig) -> None:
|
||||
await self.detach()
|
||||
self._events_source = RemoteEventsSource(session_id=session_id, config=config)
|
||||
|
||||
async def detach(self) -> None:
|
||||
await self._stop_stream()
|
||||
if self._events_source is not None:
|
||||
await self._events_source.close()
|
||||
self._events_source = None
|
||||
self._pending_waiting_input = None
|
||||
|
||||
def validate_input(self) -> str | None:
|
||||
if self.is_terminated:
|
||||
return (
|
||||
"Remote session has ended. Use /clear to start a new session"
|
||||
" or /resume to attach to another."
|
||||
)
|
||||
if not self.is_waiting_for_input:
|
||||
return (
|
||||
"Remote session is not waiting for input. Please wait for the"
|
||||
" current task to complete."
|
||||
)
|
||||
return None
|
||||
|
||||
async def send_prompt(self, message: str, *, require_source: bool = True) -> None:
|
||||
if self._events_source is None:
|
||||
if require_source:
|
||||
raise RuntimeError("No active remote session")
|
||||
return
|
||||
saved_pending = self._pending_waiting_input
|
||||
self._pending_waiting_input = None
|
||||
try:
|
||||
await self._events_source.send_prompt(message)
|
||||
except Exception:
|
||||
self._pending_waiting_input = saved_pending
|
||||
raise
|
||||
|
||||
def cancel_pending_input(self) -> None:
|
||||
self._pending_waiting_input = None
|
||||
|
||||
def build_question_args(
|
||||
self, event: WaitingForInputEvent
|
||||
) -> AskUserQuestionArgs | None:
|
||||
if (
|
||||
not event.predefined_answers
|
||||
or len(event.predefined_answers) < _MIN_QUESTION_OPTIONS
|
||||
):
|
||||
return None
|
||||
|
||||
question = event.label or "Choose an answer"
|
||||
return AskUserQuestionArgs(
|
||||
questions=[
|
||||
Question(
|
||||
question=question,
|
||||
options=[
|
||||
Choice(label=answer)
|
||||
for answer in event.predefined_answers[:_MAX_QUESTION_OPTIONS]
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
def set_pending_input(self, event: WaitingForInputEvent) -> None:
|
||||
self._pending_waiting_input = event
|
||||
|
||||
def start_stream(self, ui: RemoteSessionUI) -> None:
|
||||
if self._events_source is None:
|
||||
return
|
||||
if self._stream_task and not self._stream_task.done():
|
||||
return
|
||||
self._stream_task = asyncio.create_task(
|
||||
self._consume_stream(ui), name="remote-session-stream"
|
||||
)
|
||||
|
||||
async def stop_stream(self) -> None:
|
||||
await self._stop_stream()
|
||||
|
||||
def build_terminal_message(self) -> tuple[str, str]:
|
||||
if self._events_source is None:
|
||||
return ("info", "Remote session completed")
|
||||
if self._events_source.is_failed:
|
||||
return ("error", "Remote session failed")
|
||||
if self._events_source.is_canceled:
|
||||
return ("warning", "Remote session was canceled")
|
||||
return ("info", "Remote session completed")
|
||||
|
||||
def cancel_stream_task(self) -> None:
|
||||
if self._stream_task and not self._stream_task.done():
|
||||
self._stream_task.cancel()
|
||||
|
||||
async def _stop_stream(self) -> None:
|
||||
if self._stream_task is None or self._stream_task.done():
|
||||
self._stream_task = None
|
||||
return
|
||||
self._stream_task.cancel()
|
||||
try:
|
||||
await self._stream_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._stream_task = None
|
||||
|
||||
async def _consume_stream(self, ui: RemoteSessionUI) -> None:
|
||||
events_source = self._events_source
|
||||
if events_source is None:
|
||||
return
|
||||
await ui.ensure_loading("Generating")
|
||||
try:
|
||||
async for event in events_source.attach():
|
||||
if isinstance(event, WaitingForInputEvent):
|
||||
await ui.remove_loading()
|
||||
self._pending_waiting_input = event
|
||||
await ui.on_remote_waiting_input(event)
|
||||
elif (
|
||||
isinstance(event, UserMessageEvent)
|
||||
and self._pending_waiting_input is not None
|
||||
):
|
||||
self._pending_waiting_input = None
|
||||
await ui.on_remote_user_message_cleared_input()
|
||||
elif ui.loading_widget is None and is_progress_event(event):
|
||||
await ui.ensure_loading()
|
||||
await ui.on_remote_event(
|
||||
event,
|
||||
loading_active=ui.loading_widget is not None,
|
||||
loading_widget=ui.loading_widget,
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
await ui.on_remote_stream_error(f"Remote stream stopped: {e}")
|
||||
finally:
|
||||
await ui.on_remote_finalize_streaming()
|
||||
await ui.remove_loading()
|
||||
self._stream_task = None
|
||||
self._pending_waiting_input = None
|
||||
if events_source.is_terminated:
|
||||
msg_type, text = self.build_terminal_message()
|
||||
await ui.on_remote_stream_ended(msg_type, text)
|
||||
|
|
@ -30,6 +30,7 @@ SAFETY_BORDER_CLASSES: dict[AgentSafety, str] = {
|
|||
|
||||
class ChatInputContainer(Vertical):
|
||||
ID_INPUT_BOX = "input-box"
|
||||
REMOTE_BORDER_CLASS = "border-remote"
|
||||
|
||||
class Submitted(Message):
|
||||
def __init__(self, value: str) -> None:
|
||||
|
|
@ -59,6 +60,8 @@ class ChatInputContainer(Vertical):
|
|||
)
|
||||
self._nuage_enabled = nuage_enabled
|
||||
self._voice_manager = voice_manager
|
||||
self._custom_border_label: str | None = None
|
||||
self._custom_border_class: str | None = None
|
||||
|
||||
self._completion_manager = MultiCompletionManager([
|
||||
SlashCommandController(CommandCompleter(self._get_slash_entries), self),
|
||||
|
|
@ -86,9 +89,9 @@ class ChatInputContainer(Vertical):
|
|||
self._completion_popup = CompletionPopup()
|
||||
yield self._completion_popup
|
||||
|
||||
border_class = SAFETY_BORDER_CLASSES.get(self._safety, "")
|
||||
border_class = self._get_border_class()
|
||||
with Vertical(id=self.ID_INPUT_BOX, classes=border_class) as input_box:
|
||||
input_box.border_title = self._agent_name
|
||||
input_box.border_title = self._get_border_title()
|
||||
self._body = ChatInputBody(
|
||||
history_file=self._history_file,
|
||||
id="input-body",
|
||||
|
|
@ -195,23 +198,43 @@ class ChatInputContainer(Vertical):
|
|||
|
||||
def set_safety(self, safety: AgentSafety) -> None:
|
||||
self._safety = safety
|
||||
self._apply_input_box_chrome()
|
||||
|
||||
def set_agent_name(self, name: str) -> None:
|
||||
self._agent_name = name
|
||||
self._apply_input_box_chrome()
|
||||
|
||||
def set_custom_border(
|
||||
self, label: str | None, border_class: str | None = None
|
||||
) -> None:
|
||||
self._custom_border_label = label
|
||||
self._custom_border_class = border_class
|
||||
self._apply_input_box_chrome()
|
||||
|
||||
def _get_border_class(self) -> str:
|
||||
if self._custom_border_class is not None:
|
||||
return self._custom_border_class
|
||||
if self._custom_border_label is not None:
|
||||
return ""
|
||||
return SAFETY_BORDER_CLASSES.get(self._safety, "")
|
||||
|
||||
def _get_border_title(self) -> str:
|
||||
if self._custom_border_label is not None:
|
||||
return self._custom_border_label
|
||||
return self._agent_name
|
||||
|
||||
def _apply_input_box_chrome(self) -> None:
|
||||
try:
|
||||
input_box = self.get_widget_by_id(self.ID_INPUT_BOX)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
input_box.remove_class(self.REMOTE_BORDER_CLASS)
|
||||
for border_class in SAFETY_BORDER_CLASSES.values():
|
||||
input_box.remove_class(border_class)
|
||||
|
||||
if safety in SAFETY_BORDER_CLASSES:
|
||||
input_box.add_class(SAFETY_BORDER_CLASSES[safety])
|
||||
border_class = self._get_border_class()
|
||||
if border_class:
|
||||
input_box.add_class(border_class)
|
||||
|
||||
def set_agent_name(self, name: str) -> None:
|
||||
self._agent_name = name
|
||||
|
||||
try:
|
||||
input_box = self.get_widget_by_id(self.ID_INPUT_BOX)
|
||||
input_box.border_title = name
|
||||
except Exception:
|
||||
pass
|
||||
input_box.border_title = self._get_border_title()
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ class LoadingWidget(SpinnerMixin, Static):
|
|||
self.current_color_index = 0
|
||||
self._color_direction = 1
|
||||
self.transition_progress = 0
|
||||
self._indicator_widget: Static | None = None
|
||||
self._status_widget: Static | None = None
|
||||
self.hint_widget: Static | None = None
|
||||
self.start_time: float | None = None
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
from typing import Any, ClassVar, cast
|
||||
|
||||
from rich.text import Text
|
||||
from textual.app import ComposeResult
|
||||
|
|
@ -12,9 +12,11 @@ from textual.widgets import OptionList
|
|||
from textual.widgets.option_list import Option
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vibe.core.session.session_loader import SessionInfo
|
||||
from vibe.core.session.resume_sessions import (
|
||||
ResumeSessionInfo,
|
||||
ResumeSessionSource,
|
||||
short_session_id,
|
||||
)
|
||||
|
||||
_SECONDS_PER_MINUTE = 60
|
||||
_SECONDS_PER_HOUR = 3600
|
||||
|
|
@ -46,12 +48,15 @@ def _format_relative_time(iso_time: str | None) -> str:
|
|||
return "unknown"
|
||||
|
||||
|
||||
def _build_option_text(session: SessionInfo, message: str) -> Text:
|
||||
def _build_option_text(session: ResumeSessionInfo, message: str) -> Text:
|
||||
text = Text(no_wrap=True)
|
||||
time_str = _format_relative_time(session.get("end_time"))
|
||||
session_id = session["session_id"][:8]
|
||||
time_str = _format_relative_time(session.end_time)
|
||||
session_id = short_session_id(session.session_id, source=session.source)
|
||||
source = session.source
|
||||
text.append(f"{time_str:10}", style="dim")
|
||||
text.append(" ")
|
||||
text.append(f"{source:6}", style="cyan")
|
||||
text.append(" ")
|
||||
text.append(f"{session_id} ", style="dim")
|
||||
text.append(message)
|
||||
return text
|
||||
|
|
@ -67,7 +72,15 @@ class SessionPickerApp(Container):
|
|||
]
|
||||
|
||||
class SessionSelected(Message):
|
||||
def __init__(self, session_id: str) -> None:
|
||||
option_id: str
|
||||
source: ResumeSessionSource
|
||||
session_id: str
|
||||
|
||||
def __init__(
|
||||
self, option_id: str, source: ResumeSessionSource, session_id: str
|
||||
) -> None:
|
||||
self.option_id = option_id
|
||||
self.source = source
|
||||
self.session_id = session_id
|
||||
super().__init__()
|
||||
|
||||
|
|
@ -76,7 +89,7 @@ class SessionPickerApp(Container):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
sessions: list[SessionInfo],
|
||||
sessions: list[ResumeSessionInfo],
|
||||
latest_messages: dict[str, str],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
|
|
@ -89,9 +102,9 @@ class SessionPickerApp(Container):
|
|||
Option(
|
||||
_build_option_text(
|
||||
session,
|
||||
self._latest_messages.get(session["session_id"], "(empty session)"),
|
||||
self._latest_messages.get(session.option_id, "(empty session)"),
|
||||
),
|
||||
id=session["session_id"],
|
||||
id=session.option_id,
|
||||
)
|
||||
for session in self._sessions
|
||||
]
|
||||
|
|
@ -106,7 +119,15 @@ class SessionPickerApp(Container):
|
|||
|
||||
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
||||
if event.option.id:
|
||||
self.post_message(self.SessionSelected(event.option.id))
|
||||
option_id = event.option.id
|
||||
source, _, session_id = option_id.partition(":")
|
||||
self.post_message(
|
||||
self.SessionSelected(
|
||||
option_id=option_id,
|
||||
source=cast(ResumeSessionSource, source),
|
||||
session_id=session_id,
|
||||
)
|
||||
)
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
self.post_message(self.Cancelled())
|
||||
|
|
|
|||
|
|
@ -76,9 +76,7 @@ class ToolCallMessage(StatusMessage):
|
|||
self._stream_widget.display = True
|
||||
|
||||
def stop_spinning(self, success: bool = True) -> None:
|
||||
"""Stop the spinner and hide the stream widget."""
|
||||
if self._stream_widget:
|
||||
self._stream_widget.display = False
|
||||
"""Stop the spinner while keeping stream row stable to avoid layout jumps."""
|
||||
super().stop_spinning(success)
|
||||
|
||||
def set_result_text(self, text: str) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue