Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai>
Co-authored-by: Vincent Guilloux <vincent.guilloux@mistral.ai>
Co-authored-by: Thomas Kenbeek <thomas.kenbeek@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-02-19 12:06:03 +01:00 committed by GitHub
parent ec7f3b25ea
commit a560a47ce8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 1026 additions and 562 deletions

View file

@ -1,8 +1,12 @@
from __future__ import annotations
import base64
from collections.abc import Callable
import os
import shutil
import subprocess
import pyperclip
from textual.app import App
_PREVIEW_MAX_LENGTH = 40
@ -19,6 +23,100 @@ def _copy_osc52(text: str) -> None:
tty.flush()
def _copy_pyperclip(text: str) -> None:
pyperclip.copy(text)
def _has_cmd(cmd: str) -> bool:
return shutil.which(cmd) is not None
def _copy_pbcopy(text: str) -> None:
subprocess.run(["pbcopy"], input=text.encode("utf-8"), check=True)
def _copy_xclip(text: str) -> None:
subprocess.run(
["xclip", "-selection", "clipboard"], input=text.encode("utf-8"), check=True
)
def _copy_wl_copy(text: str) -> None:
subprocess.run(["wl-copy"], input=text.encode("utf-8"), check=True)
_CMD_STRATEGIES: list[tuple[str, Callable[[str], None]]] = [
("pbcopy", _copy_pbcopy),
("xclip", _copy_xclip),
("wl-copy", _copy_wl_copy),
]
_COPY_METHODS: list[Callable[[str], None]] = [
_copy_osc52,
_copy_pyperclip,
*[fn for cmd, fn in _CMD_STRATEGIES if _has_cmd(cmd)],
]
def _paste_pyperclip() -> str:
return pyperclip.paste()
def _paste_pbpaste() -> str:
return subprocess.run(["pbpaste"], capture_output=True, check=True).stdout.decode(
"utf-8"
)
def _paste_xclip() -> str:
return subprocess.run(
["xclip", "-selection", "clipboard", "-o"], capture_output=True, check=True
).stdout.decode("utf-8")
def _paste_wl_paste() -> str:
return subprocess.run(["wl-paste"], capture_output=True, check=True).stdout.decode(
"utf-8"
)
_PASTE_CMD_STRATEGIES: list[tuple[str, Callable[[], str]]] = [
("pbpaste", _paste_pbpaste),
("xclip", _paste_xclip),
("wl-paste", _paste_wl_paste),
]
_READ_CLIPBOARD_METHODS: list[Callable[[], str]] = [
_paste_pyperclip,
*[fn for cmd, fn in _PASTE_CMD_STRATEGIES if _has_cmd(cmd)],
]
def _read_clipboard() -> str | None:
for reader in _READ_CLIPBOARD_METHODS:
try:
return reader()
except Exception:
pass
return None
def _copy_to_clipboard(text: str) -> None:
all_strategies_failed = True
for to_clipboard in _COPY_METHODS:
try:
to_clipboard(text)
except Exception:
pass
else:
all_strategies_failed = False
if _read_clipboard() == text:
return
if all_strategies_failed:
raise RuntimeError("All clipboard strategies failed")
def _shorten_preview(texts: list[str]) -> str:
dense_text = "".join(texts).replace("\n", "")
if len(dense_text) > _PREVIEW_MAX_LENGTH:
@ -26,7 +124,7 @@ def _shorten_preview(texts: list[str]) -> str:
return dense_text
def copy_selection_to_clipboard(app: App, show_toast: bool = True) -> str | None:
def _get_selected_texts(app: App) -> list[str]:
selected_texts = []
for widget in app.query("*"):
@ -47,13 +145,17 @@ def copy_selection_to_clipboard(app: App, show_toast: bool = True) -> str | None
if selected_text.strip():
selected_texts.append(selected_text)
return selected_texts
def copy_selection_to_clipboard(app: App, show_toast: bool = True) -> str | None:
selected_texts = _get_selected_texts(app)
if not selected_texts:
return None
combined_text = "\n".join(selected_texts)
try:
_copy_osc52(combined_text)
_copy_to_clipboard(combined_text)
if show_toast:
app.notify(
f'"{_shorten_preview(selected_texts)}" copied to clipboard',

View file

@ -2,16 +2,20 @@ from __future__ import annotations
import asyncio
from enum import StrEnum, auto
import os
from pathlib import Path
import signal
import subprocess
import time
from typing import Any, ClassVar, assert_never, cast
from weakref import WeakKeyDictionary
from pydantic import BaseModel
from textual.app import App, ComposeResult
from rich import print as rprint
from textual.app import WINDOWS, App, ComposeResult
from textual.binding import Binding, BindingType
from textual.containers import Horizontal, VerticalGroup, VerticalScroll
from textual.driver import Driver
from textual.events import AppBlur, AppFocus, MouseUp
from textual.widget import Widget
from textual.widgets import Static
@ -38,11 +42,9 @@ from vibe.cli.textual_ui.widgets.context_progress import ContextProgress, TokenS
from vibe.cli.textual_ui.widgets.load_more import HistoryLoadMoreRequested
from vibe.cli.textual_ui.widgets.loading import LoadingWidget, paused_timer
from vibe.cli.textual_ui.widgets.messages import (
AssistantMessage,
BashOutputMessage,
ErrorMessage,
InterruptMessage,
ReasoningMessage,
StreamingMessageBase,
UserCommandMessage,
UserMessage,
@ -54,7 +56,7 @@ from vibe.cli.textual_ui.widgets.path_display import PathDisplay
from vibe.cli.textual_ui.widgets.proxy_setup_app import ProxySetupApp
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
from vibe.cli.textual_ui.widgets.teleport_message import TeleportMessage
from vibe.cli.textual_ui.widgets.tools import ToolCallMessage, ToolResultMessage
from vibe.cli.textual_ui.widgets.tools import ToolResultMessage
from vibe.cli.textual_ui.windowing import (
HISTORY_RESUME_TAIL_MESSAGES,
LOAD_MORE_BATCH_SIZE,
@ -180,6 +182,7 @@ class VibeApp(App): # noqa: PLR0904
BINDINGS: ClassVar[list[BindingType]] = [
Binding("ctrl+c", "clear_quit", "Quit", show=False),
Binding("ctrl+d", "force_quit", "Quit", show=False, priority=True),
Binding("ctrl+z", "suspend_with_message", "Suspend", show=False, priority=True),
Binding("escape", "interrupt", "Interrupt", show=False, priority=True),
Binding("ctrl+o", "toggle_tool", "Toggle Tool", show=False),
Binding("ctrl+y", "copy_selection", "Copy", show=False, priority=True),
@ -225,8 +228,6 @@ class VibeApp(App): # noqa: PLR0904
self.history_file = HISTORY_FILE.path
self._tools_collapsed = True
self._current_streaming_message: AssistantMessage | None = None
self._current_streaming_reasoning: ReasoningMessage | None = None
self._windowing = SessionWindowing(load_more_batch_size=LOAD_MORE_BATCH_SIZE)
self._load_more = HistoryLoadMoreManager()
self._tool_call_map: dict[str, str] | None = None
@ -239,9 +240,9 @@ class VibeApp(App): # noqa: PLR0904
self._plan_offer_gateway = plan_offer_gateway
self._initial_prompt = initial_prompt
self._teleport_on_start = teleport_on_start and self.config.nuage_enabled
self._auto_scroll = True
self._last_escape_time: float | None = None
self._banner: Banner | None = None
self._whats_new_message: WhatsNewMessage | None = None
self._cached_messages_area: Widget | None = None
self._cached_chat: ChatScroll | None = None
self._cached_loading_area: Widget | None = None
@ -267,6 +268,7 @@ class VibeApp(App): # noqa: PLR0904
safety=self.agent_loop.agent_profile.safety,
agent_name=self.agent_loop.agent_profile.display_name.lower(),
skill_entries_getter=self._get_skill_entries,
file_watcher_for_autocomplete_getter=self._is_file_watcher_enabled,
nuage_enabled=self.config.nuage_enabled,
)
@ -284,7 +286,6 @@ class VibeApp(App): # noqa: PLR0904
self.event_handler = EventHandler(
mount_callback=self._mount_and_scroll,
scroll_callback=self._scroll_to_bottom_deferred,
get_tools_collapsed=lambda: self._tools_collapsed,
)
@ -312,6 +313,8 @@ class VibeApp(App): # noqa: PLR0904
self._schedule_update_notification()
self.agent_loop.emit_new_session_telemetry("cli")
self.call_after_refresh(self._refresh_banner)
if self._initial_prompt or self._teleport_on_start:
self.call_after_refresh(self._process_initial_prompt)
@ -325,12 +328,19 @@ class VibeApp(App): # noqa: PLR0904
self._handle_user_message(self._initial_prompt), exclusive=False
)
def _is_file_watcher_enabled(self) -> bool:
return self.config.file_watcher_for_autocomplete
async def on_chat_input_container_submitted(
self, event: ChatInputContainer.Submitted
) -> None:
if self._banner:
self._banner.freeze_animation()
if self._whats_new_message:
await self._whats_new_message.remove()
self._whats_new_message = None
value = event.value.strip()
if not value:
return
@ -582,9 +592,7 @@ class VibeApp(App): # noqa: PLR0904
plan.tool_call_map,
start_index=plan.tail_start_index,
)
self.call_after_refresh(
lambda: self._align_chat_after_history_rebuild(plan.has_backfill)
)
self.call_after_refresh(self._scroll_chat_to_end)
self._tool_call_map = plan.tool_call_map
self._windowing.set_backfill(plan.backfill_messages)
await self._load_more.set_visible(
@ -703,7 +711,8 @@ class VibeApp(App): # noqa: PLR0904
if self._loading_widget:
await self._loading_widget.remove()
self._loading_widget = None
await self._finalize_current_streaming_message()
if self.event_handler:
await self.event_handler.finalize_streaming()
await self._refresh_windowing_from_history()
async def _teleport_command(self) -> None:
@ -814,6 +823,7 @@ class VibeApp(App): # noqa: PLR0904
if self.event_handler:
self.event_handler.stop_current_tool_call(success=False)
self.event_handler.stop_current_compact()
await self.event_handler.finalize_streaming()
self._agent_running = False
loading_area = self._cached_loading_area or self.query_one(
@ -822,7 +832,6 @@ class VibeApp(App): # noqa: PLR0904
await loading_area.remove_children()
self._loading_widget = None
await self._finalize_current_streaming_message()
await self._mount_and_scroll(InterruptMessage())
self._interrupt_requested = False
@ -881,7 +890,8 @@ class VibeApp(App): # noqa: PLR0904
self._tool_call_map = None
self._history_widget_indices = WeakKeyDictionary()
await self.agent_loop.clear_history()
await self._finalize_current_streaming_message()
if self.event_handler:
await self.event_handler.finalize_streaming()
messages_area = self._cached_messages_area or self.query_one("#messages")
await messages_area.remove_children()
@ -1019,7 +1029,7 @@ class VibeApp(App): # noqa: PLR0904
self.call_after_refresh(widget.focus)
if scroll:
self.call_after_refresh(self._scroll_to_bottom)
self.call_after_refresh(self._scroll_chat_to_end)
async def _switch_to_config_app(self) -> None:
if self._current_bottom_app == BottomApp.Config:
@ -1059,7 +1069,7 @@ class VibeApp(App): # noqa: PLR0904
self._chat_input_container.display = True
self._current_bottom_app = BottomApp.Input
self.call_after_refresh(self._chat_input_container.focus_input)
self.call_after_refresh(self._scroll_to_bottom)
self.call_after_refresh(self._scroll_chat_to_end)
def _focus_current_bottom_app(self) -> None:
try:
@ -1153,7 +1163,7 @@ class VibeApp(App): # noqa: PLR0904
self._handle_agent_running_escape()
self._last_escape_time = current_time
self._scroll_to_bottom()
self.call_after_refresh(self._scroll_chat_to_end)
self._focus_current_bottom_app()
async def on_history_load_more_requested(self, _: HistoryLoadMoreRequested) -> None:
@ -1211,6 +1221,10 @@ class VibeApp(App): # noqa: PLR0904
def _refresh_profile_widgets(self) -> None:
self._update_profile_widgets(self.agent_loop.agent_profile)
def _refresh_banner(self) -> None:
if self._banner:
self._banner.set_state(self.config, self.agent_loop.skill_manager)
def _update_profile_widgets(self, profile: AgentProfile) -> None:
if self._chat_input_container:
self._chat_input_container.set_safety(profile.safety)
@ -1245,7 +1259,6 @@ class VibeApp(App): # noqa: PLR0904
try:
chat = self._cached_chat or self.query_one("#chat", ChatScroll)
chat.scroll_relative(y=-5, animate=False)
self._auto_scroll = False
except Exception:
pass
@ -1253,8 +1266,6 @@ class VibeApp(App): # noqa: PLR0904
try:
chat = self._cached_chat or self.query_one("#chat", ChatScroll)
chat.scroll_relative(y=5, animate=False)
if self._is_scrolled_to_bottom(chat):
self._auto_scroll = True
except Exception:
pass
@ -1283,7 +1294,11 @@ class VibeApp(App): # noqa: PLR0904
whats_new_message = WhatsNewMessage(f"{content}\n\n{plan_offer}")
if self._history_widget_indices:
whats_new_message.add_class("after-history")
await self._mount_and_scroll(whats_new_message)
messages_area = self._cached_messages_area or self.query_one("#messages")
chat = self._cached_chat or self.query_one("#chat", ChatScroll)
await chat.mount(whats_new_message, after=messages_area)
self._whats_new_message = whats_new_message
chat.anchor()
await mark_version_as_seen(self._current_version, self._update_cache_repository)
async def _plan_offer_cta(self) -> str | None:
@ -1309,105 +1324,29 @@ class VibeApp(App): # noqa: PLR0904
)
return
async def _finalize_current_streaming_message(self) -> None:
if self._current_streaming_reasoning is not None:
self._current_streaming_reasoning.stop_spinning()
await self._current_streaming_reasoning.stop_stream()
self._current_streaming_reasoning = None
if self._current_streaming_message is None:
return
await self._current_streaming_message.stop_stream()
self._current_streaming_message = None
async def _handle_streaming_widget[T: StreamingMessageBase](
self,
widget: T,
current_stream: T | None,
other_stream: StreamingMessageBase | None,
messages_area: Widget,
) -> T | None:
if other_stream is not None:
await other_stream.stop_stream()
if current_stream is not None:
if widget._content:
await current_stream.append_content(widget._content)
return None
await messages_area.mount(widget)
await widget.write_initial_content()
return widget
def _scroll_chat_to_end(self) -> None:
chat = self._cached_chat or self.query_one("#chat", ChatScroll)
chat.scroll_end(animate=False)
async def _mount_and_scroll(self, widget: Widget) -> None:
messages_area = self._cached_messages_area or self.query_one("#messages")
chat = self._cached_chat or self.query_one("#chat", ChatScroll)
was_at_bottom = self._is_scrolled_to_bottom(chat)
result: Widget | None = None
if was_at_bottom:
self._auto_scroll = True
await messages_area.mount(widget)
if isinstance(widget, StreamingMessageBase):
await widget.write_initial_content()
if isinstance(widget, ReasoningMessage):
result = await self._handle_streaming_widget(
widget,
self._current_streaming_reasoning,
self._current_streaming_message,
messages_area,
)
if result is not None:
self._current_streaming_reasoning = result
self._current_streaming_message = None
elif isinstance(widget, AssistantMessage):
if self._current_streaming_reasoning is not None:
self._current_streaming_reasoning.stop_spinning()
result = await self._handle_streaming_widget(
widget,
self._current_streaming_message,
self._current_streaming_reasoning,
messages_area,
)
if result is not None:
self._current_streaming_message = result
self._current_streaming_reasoning = None
else:
await self._finalize_current_streaming_message()
await messages_area.mount(widget)
result = widget
is_tool_message = isinstance(widget, (ToolCallMessage, ToolResultMessage))
if not is_tool_message:
self.call_after_refresh(self._scroll_to_bottom)
if result is not None:
self.call_after_refresh(self._try_prune)
if was_at_bottom:
self.call_after_refresh(self._anchor_if_scrollable)
def _is_scrolled_to_bottom(self, scroll_view: VerticalScroll) -> bool:
try:
threshold = 3
return scroll_view.scroll_y >= (scroll_view.max_scroll_y - threshold)
except Exception:
return True
def _scroll_to_bottom(self) -> None:
try:
chat = self._cached_chat or self.query_one("#chat", ChatScroll)
chat.scroll_end(animate=False)
except Exception:
pass
def _scroll_to_bottom_deferred(self) -> None:
self.call_after_refresh(self._scroll_to_bottom)
self.call_after_refresh(self._try_prune)
chat.anchor()
async def _try_prune(self) -> None:
messages_area = self._cached_messages_area or self.query_one("#messages")
await prune_by_height(messages_area, PRUNE_LOW_MARK, PRUNE_HIGH_MARK)
pruned = await prune_by_height(messages_area, PRUNE_LOW_MARK, PRUNE_HIGH_MARK)
if self._load_more.widget and not self._load_more.widget.parent:
self._load_more.widget = None
if pruned:
chat = self._cached_chat or self.query_one("#chat", ChatScroll)
self.call_later(chat.anchor)
async def _refresh_windowing_from_history(self) -> None:
if self._load_more.widget is None:
@ -1424,29 +1363,6 @@ class VibeApp(App): # noqa: PLR0904
messages_area, visible=has_backfill, remaining=self._windowing.remaining
)
def _anchor_if_scrollable(self) -> None:
if not self._auto_scroll:
return
try:
chat = self._cached_chat or self.query_one("#chat", ChatScroll)
if chat.max_scroll_y == 0:
return
chat.anchor()
except Exception:
pass
def _align_chat_after_history_rebuild(self, has_backfill: bool) -> None:
try:
chat = self._cached_chat or self.query_one("#chat", ChatScroll)
if has_backfill and chat.max_scroll_y > 0:
chat.anchor(True)
chat.scroll_end(animate=False)
chat.anchor(False)
return
chat.scroll_end(animate=False)
except Exception:
pass
def _schedule_update_notification(self) -> None:
if self._update_notifier is None or not self.config.enable_update_checks:
return
@ -1516,6 +1432,20 @@ class VibeApp(App): # noqa: PLR0904
if self._chat_input_container and self._chat_input_container.input_widget:
self._chat_input_container.input_widget.set_app_focus(True)
def action_suspend_with_message(self) -> None:
if WINDOWS or self._driver is None or not self._driver.can_suspend:
return
with self.suspend():
rprint(
"Mistral Vibe has been suspended. Run [bold cyan]fg[/bold cyan] to bring Mistral Vibe back."
)
os.kill(os.getpid(), signal.SIGTSTP)
def _on_driver_signal_resume(self, event: Driver.SignalResume) -> None:
# Textual doesn't repaint after resuming from Ctrl+Z (SIGTSTP);
# force a full layout refresh so the UI isn't garbled.
self.refresh(layout=True)
def _print_session_resume_message(session_id: str | None) -> None:
if not session_id:

View file

@ -29,6 +29,7 @@ TextArea > .text-area--cursor {
width: 100%;
background: transparent;
padding: 0;
align-vertical: bottom;
}
#loading-area {
@ -147,8 +148,9 @@ Markdown {
color: ansi_default;
.code_inline {
color: ansi_default;
background: ansi_bright_black;
color: ansi_yellow;
background: transparent;
text-style: bold;
}
MarkdownFence {

View file

@ -27,16 +27,14 @@ if TYPE_CHECKING:
class EventHandler:
def __init__(
self,
mount_callback: Callable,
scroll_callback: Callable,
get_tools_collapsed: Callable[[], bool],
self, mount_callback: Callable, get_tools_collapsed: Callable[[], bool]
) -> None:
self.mount_callback = mount_callback
self.scroll_callback = scroll_callback
self.get_tools_collapsed = get_tools_collapsed
self.current_tool_call: ToolCallMessage | None = None
self.current_compact: CompactMessage | None = None
self.current_streaming_message: AssistantMessage | None = None
self.current_streaming_reasoning: ReasoningMessage | None = None
async def handle_event(
self,
@ -45,24 +43,29 @@ class EventHandler:
loading_widget: LoadingWidget | None = None,
) -> ToolCallMessage | None:
match event:
case ToolCallEvent():
return await self._handle_tool_call(event, loading_widget)
case ToolResultEvent():
sanitized_event = self._sanitize_event(event)
await self._handle_tool_result(sanitized_event)
case ToolStreamEvent():
await self._handle_tool_stream(event)
case ReasoningEvent():
await self._handle_reasoning_message(event)
case AssistantEvent():
await self._handle_assistant_message(event)
case ToolCallEvent():
await self.finalize_streaming()
return await self._handle_tool_call(event, loading_widget)
case ToolResultEvent():
await self.finalize_streaming()
sanitized_event = self._sanitize_event(event)
await self._handle_tool_result(sanitized_event)
case ToolStreamEvent():
await self._handle_tool_stream(event)
case CompactStartEvent():
await self.finalize_streaming()
await self._handle_compact_start()
case CompactEndEvent():
await self.finalize_streaming()
await self._handle_compact_end(event)
case UserMessageEvent():
pass
await self.finalize_streaming()
case _:
await self.finalize_streaming()
await self._handle_unknown_event(event)
return None
@ -113,13 +116,30 @@ class EventHandler:
self.current_tool_call.set_stream_message(event.message)
async def _handle_assistant_message(self, event: AssistantEvent) -> None:
await self.mount_callback(AssistantMessage(event.content))
if self.current_streaming_reasoning is not None:
self.current_streaming_reasoning.stop_spinning()
await self.current_streaming_reasoning.stop_stream()
self.current_streaming_reasoning = None
if self.current_streaming_message is None:
msg = AssistantMessage(event.content)
self.current_streaming_message = msg
await self.mount_callback(msg)
else:
await self.current_streaming_message.append_content(event.content)
async def _handle_reasoning_message(self, event: ReasoningEvent) -> None:
tools_collapsed = self.get_tools_collapsed()
await self.mount_callback(
ReasoningMessage(event.content, collapsed=tools_collapsed)
)
if self.current_streaming_message is not None:
await self.current_streaming_message.stop_stream()
self.current_streaming_message = None
if self.current_streaming_reasoning is None:
tools_collapsed = self.get_tools_collapsed()
msg = ReasoningMessage(event.content, collapsed=tools_collapsed)
self.current_streaming_reasoning = msg
await self.mount_callback(msg)
else:
await self.current_streaming_reasoning.append_content(event.content)
async def _handle_compact_start(self) -> None:
compact_msg = CompactMessage()
@ -136,6 +156,15 @@ class EventHandler:
async def _handle_unknown_event(self, event: BaseEvent) -> None:
await self.mount_callback(NoMarkupStatic(str(event), classes="unknown-event"))
async def finalize_streaming(self) -> None:
if self.current_streaming_reasoning is not None:
self.current_streaming_reasoning.stop_spinning()
await self.current_streaming_reasoning.stop_stream()
self.current_streaming_reasoning = None
if self.current_streaming_message is not None:
await self.current_streaming_message.stop_stream()
self.current_streaming_message = None
def stop_current_tool_call(self, success: bool = True) -> None:
if self.current_tool_call:
self.current_tool_call.stop_spinning(success=success)

View file

@ -42,6 +42,7 @@ class ChatInputContainer(Vertical):
safety: AgentSafety = AgentSafety.NEUTRAL,
agent_name: str = "",
skill_entries_getter: Callable[[], list[tuple[str, str]]] | None = None,
file_watcher_for_autocomplete_getter: Callable[[], bool] | None = None,
nuage_enabled: bool = False,
**kwargs: Any,
) -> None:
@ -51,11 +52,19 @@ class ChatInputContainer(Vertical):
self._safety = safety
self._agent_name = agent_name
self._skill_entries_getter = skill_entries_getter
self._file_watcher_for_autocomplete_getter = (
file_watcher_for_autocomplete_getter
)
self._nuage_enabled = nuage_enabled
self._completion_manager = MultiCompletionManager([
SlashCommandController(CommandCompleter(self._get_slash_entries), self),
PathCompletionController(PathCompleter(), self),
PathCompletionController(
PathCompleter(
watcher_enabled_getter=self._file_watcher_for_autocomplete_getter
),
self,
),
])
self._completion_popup: CompletionPopup | None = None
self._body: ChatInputBody | None = None

View file

@ -63,6 +63,12 @@ class ConfigApp(Container):
"type": "cycle",
"options": ["On", "Off"],
},
{
"key": "file_watcher_for_autocomplete",
"label": "Autocomplete watcher (may delay first autocompletion)",
"type": "cycle",
"options": ["On", "Off"],
},
]
self.title_widget: Static | None = None

View file

@ -25,9 +25,9 @@ from vibe.core.tools.builtins.write_file import WriteFileArgs, WriteFileResult
def _truncate_lines(content: str, max_lines: int) -> tuple[str, str | None]:
"""Truncate content to max_lines, returning (content, truncation_info)."""
lines = content.split("\n")
lines = content.strip("\n").split("\n")
if len(lines) <= max_lines:
return content, None
return "\n".join(lines), None
remaining = len(lines) - max_lines
return "\n".join(lines[:max_lines]), f"… ({remaining} more lines)"

View file

@ -5,7 +5,6 @@ from weakref import WeakKeyDictionary
from textual.widget import Widget
from vibe.cli.textual_ui.widgets.messages import WhatsNewMessage
from vibe.cli.textual_ui.windowing.history import (
build_tool_call_map,
split_history_tail,
@ -29,10 +28,7 @@ class HistoryResumePlan:
def should_resume_history(messages_children: list[Widget]) -> bool:
allowed_pre_existing_types = (WhatsNewMessage,)
return all(
isinstance(child, allowed_pre_existing_types) for child in messages_children
)
return len(messages_children) == 0
def create_resume_plan(