Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-03-26 10:11:34 +01:00 committed by GitHub
parent 5ea69b547b
commit 6a50d1d521
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 3726 additions and 99 deletions

View file

@ -28,7 +28,7 @@ class TestACPInitialize:
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
)
assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.6.2"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.7.0"
)
assert response.auth_methods == []
@ -52,7 +52,7 @@ class TestACPInitialize:
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
)
assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.6.2"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.7.0"
)
assert response.auth_methods is not None

View file

@ -13,9 +13,11 @@ the tests will be. Always prefer real API data over manually constructed example
from __future__ import annotations
import json
from unittest.mock import MagicMock, patch
from typing import ClassVar, Literal
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
from mistralai.client.errors import SDKError
from mistralai.client.models import AssistantMessage
from mistralai.client.utils.retries import BackoffStrategy, RetryConfig
import pytest
@ -38,7 +40,7 @@ from vibe.core.config import ModelConfig, ProviderConfig
from vibe.core.llm.backend.factory import BACKEND_FACTORY
from vibe.core.llm.backend.generic import GenericBackend
from vibe.core.llm.backend.mistral import MistralBackend, MistralMapper
from vibe.core.llm.exceptions import BackendError
from vibe.core.llm.exceptions import BackendError, BackendErrorBuilder
from vibe.core.llm.types import BackendLike
from vibe.core.types import Backend, FunctionCall, LLMChunk, LLMMessage, Role, ToolCall
from vibe.core.utils import get_user_agent
@ -526,3 +528,237 @@ class TestMistralMapperPrepareMessage:
msg = LLMMessage(role=Role.assistant, content="Hello!")
result = mapper.prepare_message(msg)
assert result.content == "Hello!"
def test_strip_reasoning_removes_reasoning_from_assistant(
self, mapper: MistralMapper
) -> None:
msg = LLMMessage(
role=Role.assistant,
content="Answer",
reasoning_content="thinking...",
reasoning_signature="sig",
)
stripped = mapper.strip_reasoning(msg)
assert stripped.content == "Answer"
assert stripped.reasoning_content is None
assert stripped.reasoning_signature is None
def test_strip_reasoning_leaves_non_assistant_unchanged(
self, mapper: MistralMapper
) -> None:
msg = LLMMessage(role=Role.user, content="hello")
assert mapper.strip_reasoning(msg) is msg
class TestMistralBackendReasoningEffort:
"""Tests that MistralBackend correctly passes reasoning_effort to the SDK."""
@pytest.fixture
def backend(self) -> MistralBackend:
provider = ProviderConfig(
name="mistral",
api_base="https://api.mistral.ai/v1",
api_key_env_var="API_KEY",
)
return MistralBackend(provider=provider)
@pytest.mark.asyncio
@pytest.mark.parametrize(
("thinking", "expected_effort", "expected_temperature"),
[
("off", None, 0.2),
("low", "none", 1.0),
("medium", "high", 1.0),
("high", "high", 1.0),
],
)
async def test_complete_passes_reasoning_effort(
self,
backend: MistralBackend,
thinking: Literal["off", "low", "medium", "high"],
expected_effort: str | None,
expected_temperature: float,
) -> None:
model = ModelConfig(
name="mistral-small-latest",
provider="mistral",
alias="mistral-small",
thinking=thinking,
)
messages = [LLMMessage(role=Role.user, content="hi")]
with patch.object(backend, "_get_client") as mock_get_client:
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "hello"
mock_response.choices[0].message.tool_calls = None
mock_response.usage.prompt_tokens = 10
mock_response.usage.completion_tokens = 5
mock_client.chat.complete_async = AsyncMock(return_value=mock_response)
mock_get_client.return_value = mock_client
await backend.complete(
model=model,
messages=messages,
temperature=0.2,
tools=None,
max_tokens=None,
tool_choice=None,
extra_headers=None,
)
call_kwargs = mock_client.chat.complete_async.call_args.kwargs
assert call_kwargs["reasoning_effort"] == expected_effort
assert call_kwargs["temperature"] == expected_temperature
@pytest.mark.asyncio
async def test_complete_strips_reasoning_when_thinking_off(
self, backend: MistralBackend
) -> None:
model = ModelConfig(
name="mistral-small-latest",
provider="mistral",
alias="mistral-small",
thinking="off",
)
messages = [
LLMMessage(role=Role.user, content="hi"),
LLMMessage(
role=Role.assistant, content="answer", reasoning_content="thinking..."
),
LLMMessage(role=Role.user, content="follow up"),
]
with patch.object(backend, "_get_client") as mock_get_client:
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "response"
mock_response.choices[0].message.tool_calls = None
mock_response.usage.prompt_tokens = 10
mock_response.usage.completion_tokens = 5
mock_client.chat.complete_async = AsyncMock(return_value=mock_response)
mock_get_client.return_value = mock_client
await backend.complete(
model=model,
messages=messages,
temperature=0.2,
tools=None,
max_tokens=None,
tool_choice=None,
extra_headers=None,
)
call_kwargs = mock_client.chat.complete_async.call_args.kwargs
assert call_kwargs["reasoning_effort"] is None
# The assistant message should have reasoning stripped
converted_msgs = call_kwargs["messages"]
assistant_msg = converted_msgs[1]
assert isinstance(assistant_msg, AssistantMessage)
assert assistant_msg.content == "answer"
class TestBuildHttpErrorBodyReading:
_MESSAGES: ClassVar[list[LLMMessage]] = [LLMMessage(role=Role.user, content="hi")]
_COMMON_KWARGS: ClassVar[dict] = dict(
provider="test",
endpoint="https://api.test.com",
model="test-model",
messages=_MESSAGES,
temperature=0.2,
has_tools=False,
tool_choice=None,
)
def _make_sdk_error(self, response: httpx.Response) -> SDKError:
return SDKError("sdk error", response)
def _make_http_status_error(
self, response: httpx.Response
) -> httpx.HTTPStatusError:
return httpx.HTTPStatusError(
"http error", request=response.request, response=response
)
def test_sdk_error_readable_body(self) -> None:
response = httpx.Response(
400,
json={"message": "invalid temperature"},
request=httpx.Request("POST", "https://api.test.com"),
)
err = BackendErrorBuilder.build_http_error(
error=self._make_sdk_error(response), **self._COMMON_KWARGS
)
assert err.status == 400
assert err.parsed_error == "invalid temperature"
assert "invalid temperature" in err.body_text
def test_http_status_error_readable_body(self) -> None:
response = httpx.Response(
400,
json={"message": "invalid temperature"},
request=httpx.Request("POST", "https://api.test.com"),
)
err = BackendErrorBuilder.build_http_error(
error=self._make_http_status_error(response), **self._COMMON_KWARGS
)
assert err.status == 400
assert err.parsed_error == "invalid temperature"
assert "invalid temperature" in err.body_text
def test_sdk_error_stream_response_falls_back_to_read(self) -> None:
response = httpx.Response(
400,
stream=httpx.ByteStream(b'{"message": "context too long"}'),
request=httpx.Request("POST", "https://api.test.com"),
)
sdk_err = SDKError(
"sdk error", response, body='{"message": "context too long"}'
)
err = BackendErrorBuilder.build_http_error(error=sdk_err, **self._COMMON_KWARGS)
assert err.parsed_error == "context too long"
assert "context too long" in err.body_text
def test_http_status_error_stream_response_falls_back_to_read(self) -> None:
response = httpx.Response(
400,
stream=httpx.ByteStream(b'{"message": "context too long"}'),
request=httpx.Request("POST", "https://api.test.com"),
)
err = BackendErrorBuilder.build_http_error(
error=self._make_http_status_error(response), **self._COMMON_KWARGS
)
assert err.parsed_error == "context too long"
assert "context too long" in err.body_text
def test_sdk_error_unreadable_response_falls_back_to_str(self) -> None:
response = MagicMock(spec=httpx.Response)
response.status_code = 400
response.reason_phrase = "Bad Request"
response.headers = {}
type(response).text = property(lambda self: (_ for _ in ()).throw(Exception))
response.read.side_effect = Exception("closed")
sdk_err = SDKError("sdk msg", response, body='{"message": "context too long"}')
err = BackendErrorBuilder.build_http_error(error=sdk_err, **self._COMMON_KWARGS)
assert err.body_text == '{"message": "context too long"}'
assert err.parsed_error == "context too long"
def test_http_status_error_unreadable_response_falls_back_to_str(self) -> None:
response = MagicMock(spec=httpx.Response)
response.status_code = 400
response.reason_phrase = "Bad Request"
response.headers = {}
type(response).text = property(lambda self: (_ for _ in ()).throw(Exception))
response.read.side_effect = Exception("closed")
response.request = httpx.Request("POST", "https://api.test.com")
http_err = httpx.HTTPStatusError(
"http error with details", request=response.request, response=response
)
err = BackendErrorBuilder.build_http_error(
error=http_err, **self._COMMON_KWARGS
)
assert "http error with details" in err.body_text

View file

@ -0,0 +1,47 @@
from __future__ import annotations
from vibe.cli.textual_ui.windowing.state import LOAD_MORE_BATCH_SIZE, SessionWindowing
from vibe.core.types import LLMMessage, Role
def _msg(
role: Role = Role.user, *, content: str = "x", injected: bool = False
) -> LLMMessage:
return LLMMessage(role=role, content=content, injected=injected)
def test_recompute_backfill_keeps_cursor_when_oldest_widgets_skip_injected_prefix() -> (
None
):
"""min(visible_indices) can sit past injected-only tail slots; backfill must not overlap."""
w = SessionWindowing(LOAD_MORE_BATCH_SIZE)
w.set_backfill([_msg() for _ in range(80)])
assert w.remaining == 80
history = [
*[_msg() for _ in range(80)],
*[_msg(injected=True) for _ in range(5)],
_msg(content="visible"),
]
visible_indices = [85]
has_backfill = w.recompute_backfill(
history, visible_indices=visible_indices, visible_history_widgets_count=1
)
assert has_backfill
assert w.remaining == 80
def test_recompute_backfill_advances_cursor_when_prefix_was_pruned_not_injected() -> (
None
):
"""If DOM lost widgets for non-injected messages, align cursor with oldest remaining widget."""
history = [_msg() for _ in range(100)]
w = SessionWindowing(LOAD_MORE_BATCH_SIZE)
w.set_backfill(history[:70])
assert w.remaining == 70
has_backfill = w.recompute_backfill(
history, visible_indices=[80], visible_history_widgets_count=10
)
assert has_backfill
assert w.remaining == 80

View file

@ -0,0 +1,308 @@
"""Integration tests for the rewind feature through agent_loop.act().
These tests drive the full pipeline:
act() create_checkpoint() _process_one_tool_call()
get_file_snapshot() add_snapshot() tool writes file
rewind_to_message() verify file restoration.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from tests.conftest import build_test_agent_loop, build_test_vibe_config
from tests.mock.utils import mock_llm_chunk
from tests.stubs.fake_backend import FakeBackend
from vibe.core.agents.models import BuiltinAgentName
from vibe.core.types import BaseEvent, FunctionCall, ToolCall
async def _act_and_collect(agent_loop, prompt: str) -> list[BaseEvent]:
return [ev async for ev in agent_loop.act(prompt)]
def _write_file_tool_call(
path: str, content: str, *, call_id: str = "call_1", overwrite: bool = False
) -> ToolCall:
args = json.dumps({"path": path, "content": content, "overwrite": overwrite})
return ToolCall(
id=call_id, index=0, function=FunctionCall(name="write_file", arguments=args)
)
def _search_replace_tool_call(
file_path: str, search: str, replace: str, *, call_id: str = "call_1"
) -> ToolCall:
content = f"<<<<<<< SEARCH\n{search}\n=======\n{replace}\n>>>>>>> REPLACE"
args = json.dumps({"file_path": file_path, "content": content})
return ToolCall(
id=call_id,
index=0,
function=FunctionCall(name="search_replace", arguments=args),
)
def _bash_tool_call(command: str, *, call_id: str = "call_1") -> ToolCall:
args = json.dumps({"command": command})
return ToolCall(
id=call_id, index=0, function=FunctionCall(name="bash", arguments=args)
)
def _make_agent_loop(backend: FakeBackend):
config = build_test_vibe_config(
enabled_tools=["write_file", "search_replace", "bash"],
tools={
"write_file": {"permission": "always"},
"search_replace": {"permission": "always"},
"bash": {"permission": "always"},
},
system_prompt_id="tests",
include_project_context=False,
include_prompt_detail=False,
)
return build_test_agent_loop(
config=config, agent_name=BuiltinAgentName.AUTO_APPROVE, backend=backend
)
@pytest.mark.asyncio
class TestRewindIntegration:
async def test_write_file_rewind_restores_original(
self, tmp_working_directory: Path
) -> None:
"""Write a file in turn 1, rewind to turn 1 → file should not exist."""
target = tmp_working_directory / "hello.txt"
backend = FakeBackend([
[
mock_llm_chunk(
content="Creating file.",
tool_calls=[_write_file_tool_call(str(target), "hello world")],
)
],
[mock_llm_chunk(content="Done.")],
])
agent_loop = _make_agent_loop(backend)
await _act_and_collect(agent_loop, "create hello.txt")
assert target.read_text() == "hello world"
rm = agent_loop.rewind_manager
rewindable = rm.get_rewindable_messages()
assert len(rewindable) == 1
await rm.rewind_to_message(rewindable[0][0], restore_files=True)
assert not target.exists()
async def test_search_replace_rewind_restores_previous_version(
self, tmp_working_directory: Path
) -> None:
"""Edit a pre-existing file with search_replace, rewind restores original."""
target = tmp_working_directory / "config.yaml"
target.write_text("key: original\n", encoding="utf-8")
backend = FakeBackend([
[
mock_llm_chunk(
content="Updating config.",
tool_calls=[
_search_replace_tool_call(
str(target), "key: original", "key: modified"
)
],
)
],
[mock_llm_chunk(content="Updated.")],
])
agent_loop = _make_agent_loop(backend)
await _act_and_collect(agent_loop, "update config")
assert target.read_text() == "key: modified\n"
rm = agent_loop.rewind_manager
rewindable = rm.get_rewindable_messages()
await rm.rewind_to_message(rewindable[0][0], restore_files=True)
assert target.read_text() == "key: original\n"
async def test_write_then_search_replace_rewind_to_middle(
self, tmp_working_directory: Path
) -> None:
"""Turn 1 creates a file with write_file, turn 2 patches it with
search_replace. Rewind to turn 2 restores the turn 1 version.
"""
target = tmp_working_directory / "app.py"
backend = FakeBackend([
# Turn 1: create the file
[
mock_llm_chunk(
content="Creating.",
tool_calls=[
_write_file_tool_call(str(target), "def main():\n pass\n")
],
)
],
[mock_llm_chunk(content="Created.")],
# Turn 2: patch with search_replace
[
mock_llm_chunk(
content="Updating.",
tool_calls=[
_search_replace_tool_call(
str(target),
" pass",
' print("hello")',
call_id="call_2",
)
],
)
],
[mock_llm_chunk(content="Updated.")],
])
agent_loop = _make_agent_loop(backend)
await _act_and_collect(agent_loop, "create app.py")
assert "pass" in target.read_text()
await _act_and_collect(agent_loop, "update app.py")
assert 'print("hello")' in target.read_text()
rm = agent_loop.rewind_manager
rewindable = rm.get_rewindable_messages()
assert len(rewindable) == 2
await rm.rewind_to_message(rewindable[1][0], restore_files=True)
assert target.read_text() == "def main():\n pass\n"
async def test_rewind_without_restore_keeps_files(
self, tmp_working_directory: Path
) -> None:
"""Rewind with restore_files=False keeps the file as-is."""
target = tmp_working_directory / "data.json"
backend = FakeBackend([
[
mock_llm_chunk(
content="Writing.",
tool_calls=[_write_file_tool_call(str(target), '{"a": 1}')],
)
],
[mock_llm_chunk(content="Done.")],
])
agent_loop = _make_agent_loop(backend)
await _act_and_collect(agent_loop, "write data")
assert target.read_text() == '{"a": 1}'
rm = agent_loop.rewind_manager
rewindable = rm.get_rewindable_messages()
await rm.rewind_to_message(rewindable[0][0], restore_files=False)
assert target.read_text() == '{"a": 1}'
async def test_rewind_then_new_turn(self, tmp_working_directory: Path) -> None:
"""After rewind, a new turn creates fresh checkpoints that work correctly."""
target = tmp_working_directory / "code.py"
backend = FakeBackend([
# Turn 1
[
mock_llm_chunk(
content="v1.", tool_calls=[_write_file_tool_call(str(target), "v1")]
)
],
[mock_llm_chunk(content="ok")],
# Turn 2
[
mock_llm_chunk(
content="v2.",
tool_calls=[
_write_file_tool_call(
str(target), "v2", call_id="call_2", overwrite=True
)
],
)
],
[mock_llm_chunk(content="ok")],
# Turn 3 (after rewind, new turn)
[
mock_llm_chunk(
content="v2bis.",
tool_calls=[
_write_file_tool_call(
str(target), "v2bis", call_id="call_3", overwrite=True
)
],
)
],
[mock_llm_chunk(content="ok")],
])
agent_loop = _make_agent_loop(backend)
await _act_and_collect(agent_loop, "turn1")
await _act_and_collect(agent_loop, "turn2")
assert target.read_text() == "v2"
rm = agent_loop.rewind_manager
rewindable = rm.get_rewindable_messages()
await rm.rewind_to_message(rewindable[1][0], restore_files=True)
assert target.read_text() == "v1"
await _act_and_collect(agent_loop, "turn2bis")
assert target.read_text() == "v2bis"
rewindable = rm.get_rewindable_messages()
await rm.rewind_to_message(rewindable[1][0], restore_files=True)
assert target.read_text() == "v1"
async def test_rewind_restores_file_deleted_by_bash(
self, tmp_working_directory: Path
) -> None:
"""A tracked file deleted via bash in turn 2 is restored on rewind.
Turn 1: create the file with write_file (file becomes tracked).
Turn 2: delete it with bash (bash has no snapshot, but
create_checkpoint re-reads known files).
Rewind to turn 2 file restored to its turn-1 content.
"""
target = tmp_working_directory / "important.txt"
backend = FakeBackend([
# Turn 1: create the file
[
mock_llm_chunk(
content="Creating.",
tool_calls=[_write_file_tool_call(str(target), "precious data")],
)
],
[mock_llm_chunk(content="Created.")],
# Turn 2: delete it via bash
[
mock_llm_chunk(
content="Deleting.",
tool_calls=[_bash_tool_call(f"rm {target}", call_id="call_2")],
)
],
[mock_llm_chunk(content="Deleted.")],
])
agent_loop = _make_agent_loop(backend)
await _act_and_collect(agent_loop, "create file")
assert target.read_text() == "precious data"
await _act_and_collect(agent_loop, "delete file")
assert not target.exists()
rm = agent_loop.rewind_manager
rewindable = rm.get_rewindable_messages()
assert len(rewindable) == 2
# Rewind to turn 2 → restores the file to its state before turn 2
await rm.rewind_to_message(rewindable[1][0], restore_files=True)
assert target.exists()
assert target.read_text() == "precious data"

View file

@ -0,0 +1,594 @@
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pytest
from vibe.core.rewind.manager import FileSnapshot, RewindError, RewindManager
from vibe.core.types import LLMMessage, MessageList, Role
def _make_messages(*contents: str) -> MessageList:
"""Create a MessageList with a system message followed by user/assistant pairs."""
msgs = MessageList([LLMMessage(role=Role.system, content="system")])
for content in contents:
msgs.append(LLMMessage(role=Role.user, content=content))
msgs.append(LLMMessage(role=Role.assistant, content=f"reply to {content}"))
return msgs
def _snap(path: Path) -> FileSnapshot:
"""Create a FileSnapshot by reading a file (or None if missing)."""
resolved = str(path.resolve())
try:
content: bytes | None = path.read_bytes()
except FileNotFoundError:
content = None
return FileSnapshot(path=resolved, content=content)
def _make_manager(
messages: MessageList,
) -> tuple[RewindManager, list[bool], list[bool]]:
save_calls: list[bool] = []
reset_calls: list[bool] = []
async def save_messages() -> None:
save_calls.append(True)
def reset_session() -> None:
reset_calls.append(True)
mgr = RewindManager(
messages=messages, save_messages=save_messages, reset_session=reset_session
)
return mgr, save_calls, reset_calls
class TestCheckpoints:
def test_create_checkpoint_carries_forward_snapshots(self, tmp_path: Path) -> None:
messages = _make_messages("hello", "world")
mgr, _, _ = _make_manager(messages)
f = tmp_path / "f.txt"
f.write_text("v1", encoding="utf-8")
mgr.create_checkpoint()
mgr.add_snapshot(_snap(f))
f.write_text("v2", encoding="utf-8")
mgr.create_checkpoint()
# Second checkpoint should have re-read the file
assert len(mgr.checkpoints) == 2
assert mgr.checkpoints[1].files[0].content == b"v2"
def test_add_snapshot_to_all_checkpoints(self, tmp_path: Path) -> None:
messages = _make_messages("hello", "world")
mgr, _, _ = _make_manager(messages)
mgr.create_checkpoint()
mgr.create_checkpoint()
f = tmp_path / "late.txt"
f.write_text("content", encoding="utf-8")
mgr.add_snapshot(_snap(f))
resolved = str(f.resolve())
assert any(s.path == resolved for s in mgr.checkpoints[0].files)
assert any(s.path == resolved for s in mgr.checkpoints[1].files)
def test_add_snapshot_no_duplicate(self, tmp_path: Path) -> None:
messages = _make_messages("hello")
mgr, _, _ = _make_manager(messages)
mgr.create_checkpoint()
f = tmp_path / "f.txt"
f.write_text("content", encoding="utf-8")
mgr.add_snapshot(_snap(f))
mgr.add_snapshot(_snap(f))
resolved = str(f.resolve())
matches = [s for s in mgr.checkpoints[0].files if s.path == resolved]
assert len(matches) == 1
def test_has_changes_detects_new_file(self, tmp_path: Path) -> None:
messages = _make_messages("hello")
mgr, _, _ = _make_manager(messages)
f = tmp_path / "new.txt"
mgr.create_checkpoint()
mgr.add_snapshot(FileSnapshot(path=str(f.resolve()), content=None))
assert not mgr.has_file_changes_at(len(messages))
f.write_text("created", encoding="utf-8")
assert mgr.has_file_changes_at(len(messages))
def test_has_changes_false_when_unchanged(self, tmp_path: Path) -> None:
messages = _make_messages("hello")
mgr, _, _ = _make_manager(messages)
f = tmp_path / "f.txt"
f.write_text("content", encoding="utf-8")
mgr.create_checkpoint()
mgr.add_snapshot(_snap(f))
assert not mgr.has_file_changes_at(len(messages))
def test_has_file_changes_at_no_checkpoint(self) -> None:
messages = _make_messages("hello")
mgr, _, _ = _make_manager(messages)
assert not mgr.has_file_changes_at(1)
class TestRewind:
def test_get_rewindable_messages(self) -> None:
messages = _make_messages("hello", "world")
mgr, _, _ = _make_manager(messages)
result = mgr.get_rewindable_messages()
assert len(result) == 2
assert result[0] == (1, "hello")
assert result[1] == (3, "world")
def test_get_rewindable_messages_excludes_injected(self) -> None:
messages = _make_messages("hello")
# Insert an injected middleware message between turns
messages.append(
LLMMessage(role=Role.user, content="plan mode reminder", injected=True)
)
messages.append(LLMMessage(role=Role.user, content="world"))
messages.append(LLMMessage(role=Role.assistant, content="reply to world"))
mgr, _, _ = _make_manager(messages)
result = mgr.get_rewindable_messages()
assert len(result) == 2
assert result[0] == (1, "hello")
# Index 3 is the injected message — it must be skipped
assert result[1] == (4, "world")
@pytest.mark.asyncio
async def test_rewind_to_message(self) -> None:
messages = _make_messages("hello", "world")
mgr, save_calls, reset_calls = _make_manager(messages)
content, errors = await mgr.rewind_to_message(3, restore_files=False)
assert content == "world"
assert errors == []
assert len(save_calls) == 1
assert len(reset_calls) == 1
assert len(messages) == 3
@pytest.mark.asyncio
async def test_rewind_to_message_invalid_index(self) -> None:
messages = _make_messages("hello")
mgr, _, _ = _make_manager(messages)
with pytest.raises(RewindError, match="Invalid message index"):
await mgr.rewind_to_message(99, restore_files=False)
@pytest.mark.asyncio
async def test_rewind_to_message_not_user(self) -> None:
messages = _make_messages("hello")
mgr, _, _ = _make_manager(messages)
with pytest.raises(RewindError, match="not a user message"):
await mgr.rewind_to_message(2, restore_files=False)
def test_messages_reset_clears_checkpoints(self) -> None:
messages = _make_messages("hello")
mgr, _, _ = _make_manager(messages)
mgr.create_checkpoint()
assert len(mgr.checkpoints) == 1
messages.reset([LLMMessage(role=Role.system, content="system")])
assert len(mgr.checkpoints) == 0
@pytest.mark.asyncio
async def test_rewind_preserves_earlier_checkpoints(self) -> None:
messages = _make_messages("hello", "world")
mgr, _, _ = _make_manager(messages)
mgr.create_checkpoint()
mgr._checkpoints[-1].message_index = 1
mgr.create_checkpoint()
mgr._checkpoints[-1].message_index = 3
assert len(mgr.checkpoints) == 2
await mgr.rewind_to_message(3, restore_files=False)
assert len(mgr.checkpoints) == 1
def test_update_system_prompt_preserves_checkpoints(self) -> None:
"""Switching agents via shift+tab calls update_system_prompt which must
NOT clear rewind checkpoints (unlike a full reset).
"""
messages = _make_messages("hello", "world")
mgr, _, _ = _make_manager(messages)
mgr.create_checkpoint()
mgr._checkpoints[-1].message_index = 1
mgr.create_checkpoint()
mgr._checkpoints[-1].message_index = 3
assert len(mgr.checkpoints) == 2
# Simulate shift+tab agent switch: only the system prompt changes
messages.update_system_prompt("new agent system prompt")
assert len(mgr.checkpoints) == 2
assert messages[0].content == "new agent system prompt"
def test_create_checkpoint_uses_current_message_count(self) -> None:
messages = _make_messages("hello")
mgr, _, _ = _make_manager(messages)
mgr.create_checkpoint()
assert mgr.checkpoints[0].message_index == len(messages)
class _Turn:
"""Helper that simulates one conversation turn."""
def __init__(self, mgr: RewindManager, messages: MessageList) -> None:
self._mgr = mgr
self._messages = messages
def begin(self, user_msg: str) -> None:
"""Start a new turn: create checkpoint then append user message.
This mirrors agent_loop.act() which calls create_checkpoint()
*before* the user message is added to the message list.
"""
self._mgr.create_checkpoint()
self._messages.append(LLMMessage(role=Role.user, content=user_msg))
def tool_write(self, path: Path, content: str) -> None:
"""Simulate a tool writing to a file (snapshot → write)."""
self._mgr.add_snapshot(_snap(path))
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
def tool_delete(self, path: Path) -> None:
"""Simulate a tool deleting a file (snapshot → unlink)."""
self._mgr.add_snapshot(_snap(path))
path.unlink()
def end(self, assistant_reply: str = "ok") -> None:
"""End the turn: append assistant reply."""
self._messages.append(LLMMessage(role=Role.assistant, content=assistant_reply))
@pytest.mark.asyncio
class TestRewindScenarios:
@staticmethod
def _setup() -> tuple[RewindManager, MessageList, _Turn]:
messages = MessageList([LLMMessage(role=Role.system, content="system")])
mgr, _, _ = _make_manager(messages)
return mgr, messages, _Turn(mgr, messages)
async def test_edit_file_across_turns_rewind_to_middle(
self, tmp_path: Path
) -> None:
"""A file is edited every turn. Rewinding restores the version from
*before* the target turn.
"""
mgr, messages, turn = self._setup()
f = tmp_path / "app.py"
f.write_text("v0", encoding="utf-8")
turn.begin("turn1")
turn.tool_write(f, "v1")
turn.end()
turn.begin("turn2")
turn.tool_write(f, "v2")
turn.end()
turn.begin("turn3")
turn.tool_write(f, "v3")
turn.end()
# Rewind to turn2 (message index 3) → file should be v1
rewindable = mgr.get_rewindable_messages()
turn2_idx = rewindable[1][0]
await mgr.rewind_to_message(turn2_idx, restore_files=True)
assert f.read_text(encoding="utf-8") == "v1"
async def test_file_created_then_rewind_before_creation(
self, tmp_path: Path
) -> None:
"""A file that didn't exist before should be deleted on rewind."""
mgr, messages, turn = self._setup()
new_file = tmp_path / "generated.py"
turn.begin("turn1")
turn.end()
turn.begin("turn2")
turn.tool_write(new_file, "print('hello')")
turn.end()
turn1_idx = mgr.get_rewindable_messages()[0][0]
await mgr.rewind_to_message(turn1_idx, restore_files=True)
assert not new_file.exists()
async def test_file_deleted_by_tool_rewind_restores(self, tmp_path: Path) -> None:
"""Rewinding past a deletion restores the file."""
mgr, _, turn = self._setup()
f = tmp_path / "config.yaml"
f.write_text("key: value", encoding="utf-8")
turn.begin("turn1")
turn.tool_write(f, "key: value") # touch to start tracking
turn.end()
turn.begin("turn2")
turn.tool_delete(f)
turn.end()
assert not f.exists()
turn2_idx = mgr.get_rewindable_messages()[1][0]
await mgr.rewind_to_message(turn2_idx, restore_files=True)
assert f.exists()
assert f.read_text(encoding="utf-8") == "key: value"
async def test_mixed_create_and_edit(self, tmp_path: Path) -> None:
"""Multiple files: one pre-existing and edited, one created mid-session."""
mgr, messages, turn = self._setup()
existing = tmp_path / "main.py"
existing.write_text("original", encoding="utf-8")
turn.begin("turn1")
turn.tool_write(existing, "modified")
turn.end()
turn.begin("turn2")
new_file = tmp_path / "utils.py"
turn.tool_write(new_file, "def helper(): ...")
turn.tool_write(existing, "modified again")
turn.end()
turn1_idx = mgr.get_rewindable_messages()[0][0]
await mgr.rewind_to_message(turn1_idx, restore_files=True)
assert existing.read_text(encoding="utf-8") == "original"
assert not new_file.exists()
async def test_user_manual_edit_between_turns(self, tmp_path: Path) -> None:
"""If the user edits a file between turns, the checkpoint at the next
turn captures the user's version, so rewind restores it.
"""
mgr, _, turn = self._setup()
f = tmp_path / "readme.md"
f.write_text("initial", encoding="utf-8")
turn.begin("turn1")
turn.tool_write(f, "tool wrote this")
turn.end()
# User manually edits the file outside the tool loop
f.write_text("user edited this", encoding="utf-8")
turn.begin("turn2")
turn.tool_write(f, "tool overwrote user")
turn.end()
# Rewind to turn2 → should restore the user's manual edit
turn2_idx = mgr.get_rewindable_messages()[1][0]
await mgr.rewind_to_message(turn2_idx, restore_files=True)
assert f.read_text(encoding="utf-8") == "user edited this"
async def test_rewind_without_restore(self, tmp_path: Path) -> None:
"""Rewinding with restore_files=False truncates messages but keeps
files as they are.
"""
mgr, messages, turn = self._setup()
f = tmp_path / "data.json"
f.write_text("{}", encoding="utf-8")
turn.begin("turn1")
turn.tool_write(f, '{"a": 1}')
turn.end()
turn.begin("turn2")
turn.tool_write(f, '{"a": 1, "b": 2}')
turn.end()
turn1_idx = mgr.get_rewindable_messages()[0][0]
await mgr.rewind_to_message(turn1_idx, restore_files=False)
# File untouched
assert f.read_text(encoding="utf-8") == '{"a": 1, "b": 2}'
# But messages were truncated
assert len(messages) == 1 # only system message
async def test_rewind_then_new_turns_then_rewind_again(
self, tmp_path: Path
) -> None:
"""After a rewind, new turns create new checkpoints. A second rewind
should work correctly with the new history.
"""
mgr, _, turn = self._setup()
f = tmp_path / "code.py"
f.write_text("v0", encoding="utf-8")
turn.begin("turn1")
turn.tool_write(f, "v1")
turn.end()
turn.begin("turn2")
turn.tool_write(f, "v2")
turn.end()
# Rewind to turn2
turn2_idx = mgr.get_rewindable_messages()[1][0]
await mgr.rewind_to_message(turn2_idx, restore_files=True)
assert f.read_text(encoding="utf-8") == "v1"
# New turn after rewind
turn.begin("turn2-bis")
turn.tool_write(f, "v2-bis")
turn.end()
turn.begin("turn3-bis")
turn.tool_write(f, "v3-bis")
turn.end()
# Rewind to turn2-bis
turn2bis_idx = mgr.get_rewindable_messages()[1][0]
await mgr.rewind_to_message(turn2bis_idx, restore_files=True)
assert f.read_text(encoding="utf-8") == "v1"
async def test_agent_switch_between_turns_preserves_rewind(
self, tmp_path: Path
) -> None:
"""Pressing shift+tab between two messages switches agents, which calls
update_system_prompt. Checkpoints must survive so a subsequent rewind
restores files correctly.
"""
mgr, messages, turn = self._setup()
f = tmp_path / "main.py"
f.write_text("v0", encoding="utf-8")
turn.begin("turn1")
turn.tool_write(f, "v1")
turn.end()
# User presses shift+tab → agent switch → system prompt replaced
messages.update_system_prompt("switched agent prompt")
turn.begin("turn2")
turn.tool_write(f, "v2")
turn.end()
# Rewind to turn2 should restore "v1"
turn2_idx = mgr.get_rewindable_messages()[1][0]
await mgr.rewind_to_message(turn2_idx, restore_files=True)
assert f.read_text(encoding="utf-8") == "v1"
async def test_binary_file_snapshot_and_restore(self, tmp_path: Path) -> None:
"""Binary files (non-UTF-8) are snapshotted and restored correctly."""
mgr, _, turn = self._setup()
f = tmp_path / "image.bin"
original = bytes(range(256))
f.write_bytes(original)
turn.begin("turn1")
mgr.add_snapshot(_snap(f))
f.write_bytes(b"\x00" * 256)
turn.end()
turn1_idx = mgr.get_rewindable_messages()[0][0]
await mgr.rewind_to_message(turn1_idx, restore_files=True)
assert f.read_bytes() == original
async def test_create_edit_delete_full_lifecycle(self, tmp_path: Path) -> None:
"""File goes through create → edit → delete. Rewind to each point
restores the correct state.
"""
mgr, _, turn = self._setup()
f = tmp_path / "temp.txt"
turn.begin("turn1")
turn.tool_write(f, "created")
turn.end()
turn.begin("turn2")
turn.tool_write(f, "edited")
turn.end()
turn.begin("turn3")
turn.tool_delete(f)
turn.end()
assert not f.exists()
# Rewind to turn3 → file should be "edited" (state before deletion)
turn3_idx = mgr.get_rewindable_messages()[2][0]
await mgr.rewind_to_message(turn3_idx, restore_files=True)
assert f.read_text(encoding="utf-8") == "edited"
async def test_user_creates_file_tool_overwrites(self, tmp_path: Path) -> None:
"""User creates a file manually before a turn. The tool overwrites it.
Rewind restores the user's version.
"""
mgr, _, turn = self._setup()
f = tmp_path / "notes.txt"
turn.begin("turn1")
turn.end()
# User creates the file manually between turns
f.write_text("user notes", encoding="utf-8")
turn.begin("turn2")
turn.tool_write(f, "overwritten by tool")
turn.end()
turn2_idx = mgr.get_rewindable_messages()[1][0]
await mgr.rewind_to_message(turn2_idx, restore_files=True)
assert f.read_text(encoding="utf-8") == "user notes"
async def test_nested_directory_files(self, tmp_path: Path) -> None:
"""Files in nested directories are restored including parent dirs."""
mgr, _, turn = self._setup()
deep = tmp_path / "src" / "pkg" / "module.py"
turn.begin("turn1")
turn.tool_write(deep, "def foo(): pass")
turn.end()
turn.begin("turn2")
turn.tool_write(deep, "def foo(): return 42")
turn.end()
turn1_idx = mgr.get_rewindable_messages()[0][0]
# Delete everything
deep.unlink()
(tmp_path / "src" / "pkg").rmdir()
(tmp_path / "src").rmdir()
await mgr.rewind_to_message(turn1_idx, restore_files=True)
# File didn't exist before turn1 → should be deleted
assert not deep.exists()
async def test_rewind_restores_errors_collected(self, tmp_path: Path) -> None:
"""When removing a file during rewind fails, errors are returned in the tuple."""
mgr, _, turn = self._setup()
created_file = tmp_path / "locked.txt"
turn.begin("turn1")
turn.end()
turn.begin("turn2")
# Snapshot runs before write → earlier checkpoints record content=None
turn.tool_write(created_file, "created in turn2")
turn.end()
turn1_idx = mgr.get_rewindable_messages()[0][0]
with patch(
"vibe.core.rewind.manager.os.remove",
side_effect=OSError("mocked removal failure"),
):
_, errors = await mgr.rewind_to_message(turn1_idx, restore_files=True)
assert len(errors) == 1
assert "Failed to delete file" in errors[0]
assert "locked.txt" in errors[0]

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

@ -0,0 +1,204 @@
<svg class="rich-terminal" viewBox="0 0 1482 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #ff8205 }
.terminal-r5 { fill: #292929;font-weight: bold }
.terminal-r6 { fill: #9a9b99 }
.terminal-r7 { fill: #cc555a }
.terminal-r8 { fill: #608ab1;font-weight: bold }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1463.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1464" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">RewindSnapshotApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#ff8205" x="24.4" y="587.1" width="158.6" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="0" y="337.2" textLength="134.2" clip-path="url(#terminal-line-13)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="337.2" textLength="146.4" clip-path="url(#terminal-line-13)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="337.2" textLength="122" clip-path="url(#terminal-line-13)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="337.2" textLength="183" clip-path="url(#terminal-line-13)">devstral-latest</text><text class="terminal-r1" x="622.2" y="337.2" textLength="256.2" clip-path="url(#terminal-line-13)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="0" y="361.6" textLength="134.2" clip-path="url(#terminal-line-14)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="361.6" textLength="414.8" clip-path="url(#terminal-line-14)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="386" textLength="61" clip-path="url(#terminal-line-15)">Type&#160;</text><text class="terminal-r3" x="231.8" y="386" textLength="61" clip-path="url(#terminal-line-15)">/help</text><text class="terminal-r1" x="292.8" y="386" textLength="256.2" clip-path="url(#terminal-line-15)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r4" x="0" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r2" x="24.4" y="459.2" textLength="158.6" clip-path="url(#terminal-line-18)">first&#160;message</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="317.2" clip-path="url(#terminal-line-20)">Hello!&#160;How&#160;can&#160;I&#160;help&#160;you?</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r4" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r2" x="24.4" y="556.8" textLength="170.8" clip-path="url(#terminal-line-22)">second&#160;message</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r4" x="0" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r5" x="24.4" y="605.6" textLength="158.6" clip-path="url(#terminal-line-24)">third&#160;message</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r6" x="0" y="678.8" textLength="719.8" clip-path="url(#terminal-line-27)">┌──────────────────────────────────────────────────────────</text><text class="terminal-r7" x="719.8" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r6" x="1439.6" y="678.8" textLength="24.4" clip-path="url(#terminal-line-27)">─┐</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r6" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r8" x="24.4" y="703.2" textLength="292.8" clip-path="url(#terminal-line-28)">Rewind&#160;to:&#160;third&#160;message</text><text class="terminal-r7" x="719.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="744.2" y="703.2" textLength="305" clip-path="url(#terminal-line-28)">Invalid&#160;message&#160;index:&#160;99</text><text class="terminal-r6" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r6" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r7" x="719.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r6" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r6" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r8" x="24.4" y="752" textLength="475.8" clip-path="url(#terminal-line-30)">&#160;1.&#160;Edit&#160;&amp;&#160;restore&#160;files&#160;to&#160;this&#160;point</text><text class="terminal-r6" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r6" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="24.4" y="776.4" textLength="402.6" clip-path="url(#terminal-line-31)">&#160;&#160;2.&#160;Edit&#160;without&#160;restoring&#160;files</text><text class="terminal-r6" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r6" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r6" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r6" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r6" x="24.4" y="825.2" textLength="939.4" clip-path="url(#terminal-line-33)">Alt+↑↓&#160;or&#160;Ctrl+P/N&#160;browse&#160;messages&#160;&#160;↑↓&#160;pick&#160;option&#160;&#160;Enter&#160;confirm&#160;&#160;ESC&#160;cancel</text><text class="terminal-r6" x="1451.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r6" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r6" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r6" x="1256.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1,201 @@
<svg class="rich-terminal" viewBox="0 0 1482 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #ff8205 }
.terminal-r5 { fill: #9a9b99 }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1463.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1464" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">RewindSnapshotApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="410.4" textLength="183" clip-path="url(#terminal-line-16)">devstral-latest</text><text class="terminal-r1" x="622.2" y="410.4" textLength="256.2" clip-path="url(#terminal-line-16)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type&#160;</text><text class="terminal-r3" x="231.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="292.8" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r4" x="0" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r2" x="24.4" y="532.4" textLength="158.6" clip-path="url(#terminal-line-21)">first&#160;message</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="0" y="581.2" textLength="317.2" clip-path="url(#terminal-line-23)">Hello!&#160;How&#160;can&#160;I&#160;help&#160;you?</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r4" x="0" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r2" x="24.4" y="630" textLength="170.8" clip-path="url(#terminal-line-25)">second&#160;message</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r4" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r2" x="24.4" y="678.8" textLength="158.6" clip-path="url(#terminal-line-27)">third&#160;message</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="1464" clip-path="url(#terminal-line-30)">┌────────────────────────────────────────────────────────────────────────────────────────────────────────────&#160;default&#160;─┐</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r2" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">&gt;</text><text class="terminal-r5" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r5" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r5" x="1451.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r5" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r5" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r5" x="1256.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,203 @@
<svg class="rich-terminal" viewBox="0 0 1482 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #ff8205 }
.terminal-r5 { fill: #292929;font-weight: bold }
.terminal-r6 { fill: #9a9b99 }
.terminal-r7 { fill: #608ab1;font-weight: bold }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1463.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1464" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">RewindSnapshotApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#ff8205" x="24.4" y="587.1" width="158.6" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="0" y="337.2" textLength="134.2" clip-path="url(#terminal-line-13)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="337.2" textLength="146.4" clip-path="url(#terminal-line-13)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="337.2" textLength="122" clip-path="url(#terminal-line-13)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="337.2" textLength="183" clip-path="url(#terminal-line-13)">devstral-latest</text><text class="terminal-r1" x="622.2" y="337.2" textLength="256.2" clip-path="url(#terminal-line-13)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="0" y="361.6" textLength="134.2" clip-path="url(#terminal-line-14)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="361.6" textLength="414.8" clip-path="url(#terminal-line-14)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="386" textLength="61" clip-path="url(#terminal-line-15)">Type&#160;</text><text class="terminal-r3" x="231.8" y="386" textLength="61" clip-path="url(#terminal-line-15)">/help</text><text class="terminal-r1" x="292.8" y="386" textLength="256.2" clip-path="url(#terminal-line-15)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r4" x="0" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r2" x="24.4" y="459.2" textLength="158.6" clip-path="url(#terminal-line-18)">first&#160;message</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="317.2" clip-path="url(#terminal-line-20)">Hello!&#160;How&#160;can&#160;I&#160;help&#160;you?</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r4" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r2" x="24.4" y="556.8" textLength="170.8" clip-path="url(#terminal-line-22)">second&#160;message</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r4" x="0" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r5" x="24.4" y="605.6" textLength="158.6" clip-path="url(#terminal-line-24)">third&#160;message</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r6" x="0" y="678.8" textLength="1464" clip-path="url(#terminal-line-27)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r6" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r7" x="24.4" y="703.2" textLength="292.8" clip-path="url(#terminal-line-28)">Rewind&#160;to:&#160;third&#160;message</text><text class="terminal-r6" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r6" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r6" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r6" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r7" x="24.4" y="752" textLength="475.8" clip-path="url(#terminal-line-30)">&#160;1.&#160;Edit&#160;&amp;&#160;restore&#160;files&#160;to&#160;this&#160;point</text><text class="terminal-r6" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r6" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="24.4" y="776.4" textLength="402.6" clip-path="url(#terminal-line-31)">&#160;&#160;2.&#160;Edit&#160;without&#160;restoring&#160;files</text><text class="terminal-r6" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r6" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r6" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r6" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r6" x="24.4" y="825.2" textLength="939.4" clip-path="url(#terminal-line-33)">Alt+↑↓&#160;or&#160;Ctrl+P/N&#160;browse&#160;messages&#160;&#160;↑↓&#160;pick&#160;option&#160;&#160;Enter&#160;confirm&#160;&#160;ESC&#160;cancel</text><text class="terminal-r6" x="1451.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r6" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r6" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r6" x="1256.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,203 @@
<svg class="rich-terminal" viewBox="0 0 1482 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #ff8205 }
.terminal-r5 { fill: #292929;font-weight: bold }
.terminal-r6 { fill: #9a9b99 }
.terminal-r7 { fill: #608ab1;font-weight: bold }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1463.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1464" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">RewindSnapshotApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#ff8205" x="24.4" y="538.3" width="170.8" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="0" y="337.2" textLength="134.2" clip-path="url(#terminal-line-13)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="337.2" textLength="146.4" clip-path="url(#terminal-line-13)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="337.2" textLength="122" clip-path="url(#terminal-line-13)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="337.2" textLength="183" clip-path="url(#terminal-line-13)">devstral-latest</text><text class="terminal-r1" x="622.2" y="337.2" textLength="256.2" clip-path="url(#terminal-line-13)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="0" y="361.6" textLength="134.2" clip-path="url(#terminal-line-14)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="361.6" textLength="414.8" clip-path="url(#terminal-line-14)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="386" textLength="61" clip-path="url(#terminal-line-15)">Type&#160;</text><text class="terminal-r3" x="231.8" y="386" textLength="61" clip-path="url(#terminal-line-15)">/help</text><text class="terminal-r1" x="292.8" y="386" textLength="256.2" clip-path="url(#terminal-line-15)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r4" x="0" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r2" x="24.4" y="459.2" textLength="158.6" clip-path="url(#terminal-line-18)">first&#160;message</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="317.2" clip-path="url(#terminal-line-20)">Hello!&#160;How&#160;can&#160;I&#160;help&#160;you?</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r4" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r5" x="24.4" y="556.8" textLength="170.8" clip-path="url(#terminal-line-22)">second&#160;message</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r4" x="0" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r2" x="24.4" y="605.6" textLength="158.6" clip-path="url(#terminal-line-24)">third&#160;message</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r6" x="0" y="678.8" textLength="1464" clip-path="url(#terminal-line-27)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r6" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r7" x="24.4" y="703.2" textLength="305" clip-path="url(#terminal-line-28)">Rewind&#160;to:&#160;second&#160;message</text><text class="terminal-r6" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r6" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r6" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r6" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r7" x="24.4" y="752" textLength="475.8" clip-path="url(#terminal-line-30)">&#160;1.&#160;Edit&#160;&amp;&#160;restore&#160;files&#160;to&#160;this&#160;point</text><text class="terminal-r6" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r6" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="24.4" y="776.4" textLength="402.6" clip-path="url(#terminal-line-31)">&#160;&#160;2.&#160;Edit&#160;without&#160;restoring&#160;files</text><text class="terminal-r6" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r6" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r6" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r6" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r6" x="24.4" y="825.2" textLength="939.4" clip-path="url(#terminal-line-33)">Alt+↑↓&#160;or&#160;Ctrl+P/N&#160;browse&#160;messages&#160;&#160;↑↓&#160;pick&#160;option&#160;&#160;Enter&#160;confirm&#160;&#160;ESC&#160;cancel</text><text class="terminal-r6" x="1451.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r6" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r6" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r6" x="1256.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,203 @@
<svg class="rich-terminal" viewBox="0 0 1482 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #ff8205 }
.terminal-r5 { fill: #292929;font-weight: bold }
.terminal-r6 { fill: #9a9b99 }
.terminal-r7 { fill: #608ab1;font-weight: bold }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1463.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1464" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">RewindSnapshotApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#ff8205" x="24.4" y="587.1" width="158.6" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="0" y="337.2" textLength="134.2" clip-path="url(#terminal-line-13)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="337.2" textLength="146.4" clip-path="url(#terminal-line-13)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="337.2" textLength="122" clip-path="url(#terminal-line-13)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="337.2" textLength="183" clip-path="url(#terminal-line-13)">devstral-latest</text><text class="terminal-r1" x="622.2" y="337.2" textLength="256.2" clip-path="url(#terminal-line-13)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="0" y="361.6" textLength="134.2" clip-path="url(#terminal-line-14)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="361.6" textLength="414.8" clip-path="url(#terminal-line-14)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="386" textLength="61" clip-path="url(#terminal-line-15)">Type&#160;</text><text class="terminal-r3" x="231.8" y="386" textLength="61" clip-path="url(#terminal-line-15)">/help</text><text class="terminal-r1" x="292.8" y="386" textLength="256.2" clip-path="url(#terminal-line-15)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r4" x="0" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r2" x="24.4" y="459.2" textLength="158.6" clip-path="url(#terminal-line-18)">first&#160;message</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="317.2" clip-path="url(#terminal-line-20)">Hello!&#160;How&#160;can&#160;I&#160;help&#160;you?</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r4" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r2" x="24.4" y="556.8" textLength="170.8" clip-path="url(#terminal-line-22)">second&#160;message</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r4" x="0" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r5" x="24.4" y="605.6" textLength="158.6" clip-path="url(#terminal-line-24)">third&#160;message</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r6" x="0" y="678.8" textLength="1464" clip-path="url(#terminal-line-27)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r6" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r7" x="24.4" y="703.2" textLength="292.8" clip-path="url(#terminal-line-28)">Rewind&#160;to:&#160;third&#160;message</text><text class="terminal-r6" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r6" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r6" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r6" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r7" x="24.4" y="752" textLength="475.8" clip-path="url(#terminal-line-30)">&#160;1.&#160;Edit&#160;&amp;&#160;restore&#160;files&#160;to&#160;this&#160;point</text><text class="terminal-r6" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r6" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="24.4" y="776.4" textLength="402.6" clip-path="url(#terminal-line-31)">&#160;&#160;2.&#160;Edit&#160;without&#160;restoring&#160;files</text><text class="terminal-r6" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r6" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r6" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r6" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r6" x="24.4" y="825.2" textLength="939.4" clip-path="url(#terminal-line-33)">Alt+↑↓&#160;or&#160;Ctrl+P/N&#160;browse&#160;messages&#160;&#160;↑↓&#160;pick&#160;option&#160;&#160;Enter&#160;confirm&#160;&#160;ESC&#160;cancel</text><text class="terminal-r6" x="1451.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r6" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r6" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r6" x="1256.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,201 @@
<svg class="rich-terminal" viewBox="0 0 1482 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #ff8205 }
.terminal-r5 { fill: #9a9b99 }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1463.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1464" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">SnapshotTestAppWithInjectedMessages</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="410.4" textLength="183" clip-path="url(#terminal-line-16)">devstral-latest</text><text class="terminal-r1" x="622.2" y="410.4" textLength="256.2" clip-path="url(#terminal-line-16)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type&#160;</text><text class="terminal-r3" x="231.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="292.8" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r4" x="0" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r2" x="24.4" y="532.4" textLength="280.6" clip-path="url(#terminal-line-21)">Hello,&#160;can&#160;you&#160;help&#160;me?</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="0" y="581.2" textLength="280.6" clip-path="url(#terminal-line-23)">Sure!&#160;What&#160;do&#160;you&#160;need?</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r4" x="0" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r2" x="24.4" y="630" textLength="329.4" clip-path="url(#terminal-line-25)">Please&#160;read&#160;my&#160;config&#160;file.</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r1" x="0" y="678.8" textLength="488" clip-path="url(#terminal-line-27)">Here&#160;is&#160;the&#160;content&#160;of&#160;your&#160;config&#160;file.</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="1464" clip-path="url(#terminal-line-30)">┌────────────────────────────────────────────────────────────────────────────────────────────────────────────&#160;default&#160;─┐</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r2" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">&gt;</text><text class="terminal-r5" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r5" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r5" x="1451.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r5" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r5" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r5" x="1256.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -8,3 +8,8 @@ def _pin_banner_version(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"vibe.cli.textual_ui.widgets.banner.banner.__version__", "0.0.0"
)
@pytest.fixture(autouse=True)
def _pin_alt_key(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("vibe.cli.textual_ui.widgets.rewind_app.ALT_KEY", "Alt")

View file

@ -0,0 +1,142 @@
from __future__ import annotations
from unittest.mock import patch
import pytest
from textual.pilot import Pilot
from tests.mock.utils import mock_llm_chunk
from tests.snapshots.base_snapshot_test_app import BaseSnapshotTestApp
from tests.snapshots.snap_compare import SnapCompare
from tests.stubs.fake_backend import FakeBackend
from vibe.core.rewind import RewindError
class RewindSnapshotApp(BaseSnapshotTestApp):
"""Test app with a multi-turn conversation for rewind snapshots."""
def __init__(self) -> None:
fake_backend = FakeBackend([
mock_llm_chunk(content="Hello! How can I help you?")
])
super().__init__(backend=fake_backend)
async def _send_messages(pilot: Pilot) -> None:
"""Send three messages to build up conversation history.
Also patches ``has_file_changes_at`` to always return True so the
rewind panel shows the "restore files" option.
"""
for msg in ["first message", "second message", "third message"]:
await pilot.press(*msg)
await pilot.press("enter")
await pilot.pause(0.4)
app: RewindSnapshotApp = pilot.app # type: ignore[assignment]
rm = app.agent_loop.rewind_manager
patch.object(rm, "has_file_changes_at", return_value=True).start()
def test_snapshot_rewind_panel_shown(snap_compare: SnapCompare) -> None:
"""Pressing alt+up enters rewind mode and shows the panel."""
async def run_before(pilot: Pilot) -> None:
await _send_messages(pilot)
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.2)
assert snap_compare(
"test_ui_snapshot_rewind.py:RewindSnapshotApp",
terminal_size=(120, 36),
run_before=run_before,
)
def test_snapshot_rewind_navigate_up(snap_compare: SnapCompare) -> None:
"""Pressing alt+up twice selects the second-to-last message."""
async def run_before(pilot: Pilot) -> None:
await _send_messages(pilot)
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.2)
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.2)
assert snap_compare(
"test_ui_snapshot_rewind.py:RewindSnapshotApp",
terminal_size=(120, 36),
run_before=run_before,
)
def test_snapshot_rewind_navigate_down(snap_compare: SnapCompare) -> None:
"""Navigate up twice then down once returns to the last message."""
async def run_before(pilot: Pilot) -> None:
await _send_messages(pilot)
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.2)
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.2)
await pilot.press("alt+down")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.2)
assert snap_compare(
"test_ui_snapshot_rewind.py:RewindSnapshotApp",
terminal_size=(120, 36),
run_before=run_before,
)
def test_snapshot_rewind_exit_on_escape(snap_compare: SnapCompare) -> None:
"""Pressing escape exits rewind mode and restores the input panel."""
async def run_before(pilot: Pilot) -> None:
await _send_messages(pilot)
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.2)
await pilot.press("escape")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.2)
assert snap_compare(
"test_ui_snapshot_rewind.py:RewindSnapshotApp",
terminal_size=(120, 36),
run_before=run_before,
)
def test_snapshot_rewind_error_shows_toast(
snap_compare: SnapCompare, monkeypatch: pytest.MonkeyPatch
) -> None:
"""When rewind_to_message fails, a toast is shown and rewind mode stays active."""
async def failing_rewind(*_args, **_kwargs):
raise RewindError("Invalid message index: 99")
async def run_before(pilot: Pilot) -> None:
await _send_messages(pilot)
app: RewindSnapshotApp = pilot.app # type: ignore[assignment]
monkeypatch.setattr(
app.agent_loop.rewind_manager, "rewind_to_message", failing_rewind
)
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.2)
await pilot.press("enter")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.3)
assert snap_compare(
"test_ui_snapshot_rewind.py:RewindSnapshotApp",
terminal_size=(120, 36),
run_before=run_before,
)

View file

@ -0,0 +1,47 @@
from __future__ import annotations
from textual.pilot import Pilot
from tests.snapshots.base_snapshot_test_app import BaseSnapshotTestApp
from tests.snapshots.snap_compare import SnapCompare
from vibe.core.types import LLMMessage, Role
class SnapshotTestAppWithInjectedMessages(BaseSnapshotTestApp):
"""Simulates resuming a session that contains injected middleware messages.
The injected plan-mode reminder between the two user turns must not
appear in the rendered history.
"""
def __init__(self) -> None:
super().__init__()
self.agent_loop.messages.extend([
LLMMessage(role=Role.user, content="Hello, can you help me?"),
LLMMessage(role=Role.assistant, content="Sure! What do you need?"),
# Middleware-injected plan mode reminder — should be hidden
LLMMessage(
role=Role.user,
content="<vibe_warning>Plan mode is active. You MUST NOT make any edits.</vibe_warning>",
injected=True,
),
LLMMessage(role=Role.user, content="Please read my config file."),
LLMMessage(
role=Role.assistant, content="Here is the content of your config file."
),
])
def test_snapshot_session_resume_hides_injected_messages(
snap_compare: SnapCompare,
) -> None:
"""Injected middleware messages must not be rendered when resuming a session."""
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.5)
assert snap_compare(
"test_ui_snapshot_session_resume_injected.py:SnapshotTestAppWithInjectedMessages",
terminal_size=(120, 36),
run_before=run_before,
)

View file

@ -442,12 +442,16 @@ async def test_act_flushes_and_logs_when_streaming_errors(observer_capture) -> N
@pytest.mark.asyncio
async def test_rate_limit(observer_capture) -> None:
observed, observer = observer_capture
response = httpx.Response(HTTPStatus.TOO_MANY_REQUESTS)
response = httpx.Response(
HTTPStatus.TOO_MANY_REQUESTS, request=httpx.Request("POST", "http://test")
)
error = httpx.HTTPStatusError(
"rate limited", request=response.request, response=response
)
backend_error = BackendErrorBuilder.build_http_error(
provider="mistral",
endpoint="test",
response=response,
headers=None,
error=error,
model="test-model",
messages=[],
temperature=0.0,
@ -640,3 +644,27 @@ async def test_empty_content_chunks_do_not_trigger_false_yields() -> None:
("ReasoningEvent", " more reasoning"),
("AssistantEvent", "Actual content"),
]
@pytest.mark.asyncio
async def test_streaming_assistant_event_message_id_matches_stored_message() -> None:
backend = FakeBackend([
mock_llm_chunk(content="Hello"),
mock_llm_chunk(content=" world"),
])
agent = build_test_agent_loop(
config=make_config(), backend=backend, enable_streaming=True
)
events = [event async for event in agent.act("Test")]
assistant_events = [e for e in events if isinstance(e, AssistantEvent)]
assert len(assistant_events) == 2
# All chunks of the same assistant turn share one message_id
message_ids = {e.message_id for e in assistant_events}
assert len(message_ids) == 1
# The stored LLMMessage must carry that same message_id
stored_msg = next(m for m in agent.messages if m.role == Role.assistant)
assert stored_msg.message_id == assistant_events[0].message_id

230
tests/test_ui_rewind.py Normal file
View file

@ -0,0 +1,230 @@
from __future__ import annotations
import pytest
from tests.conftest import build_test_agent_loop, build_test_vibe_app
from tests.mock.utils import mock_llm_chunk
from tests.stubs.fake_backend import FakeBackend
from vibe.cli.textual_ui.app import BottomApp, VibeApp
from vibe.cli.textual_ui.widgets.chat_input.container import ChatInputContainer
from vibe.cli.textual_ui.widgets.messages import UserMessage
def _make_app(num_responses: int = 3) -> VibeApp:
backend = FakeBackend([
mock_llm_chunk(content=f"Response {i + 1}") for i in range(num_responses)
])
agent_loop = build_test_agent_loop(backend=backend)
return build_test_vibe_app(agent_loop=agent_loop)
async def _send_messages(pilot, messages: list[str]) -> None:
for msg in messages:
await pilot.press(*msg)
await pilot.press("enter")
await pilot.pause(0.4)
@pytest.mark.asyncio
async def test_rewind_mode_activates_on_alt_up() -> None:
app = _make_app()
async with app.run_test() as pilot:
await _send_messages(pilot, ["hello", "world"])
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
assert app._rewind_mode is True
assert app._current_bottom_app == BottomApp.Rewind
@pytest.mark.asyncio
async def test_rewind_highlights_last_user_message() -> None:
app = _make_app()
async with app.run_test() as pilot:
await _send_messages(pilot, ["hello", "world"])
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
assert app._rewind_highlighted_widget is not None
assert app._rewind_highlighted_widget._content == "world"
@pytest.mark.asyncio
async def test_rewind_navigates_to_previous_message() -> None:
app = _make_app()
async with app.run_test() as pilot:
await _send_messages(pilot, ["hello", "world"])
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
assert app._rewind_highlighted_widget is not None
assert app._rewind_highlighted_widget._content == "hello"
@pytest.mark.asyncio
async def test_rewind_navigates_down() -> None:
app = _make_app()
async with app.run_test() as pilot:
await _send_messages(pilot, ["hello", "world"])
# Go up twice, then down once
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
await pilot.press("alt+down")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
assert app._rewind_highlighted_widget is not None
assert app._rewind_highlighted_widget._content == "world"
@pytest.mark.asyncio
async def test_rewind_escape_exits_mode() -> None:
app = _make_app()
async with app.run_test() as pilot:
await _send_messages(pilot, ["hello", "world"])
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
await pilot.press("escape")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
assert app._rewind_mode is False
assert app._rewind_highlighted_widget is None
assert app._current_bottom_app == BottomApp.Input
@pytest.mark.asyncio
async def test_rewind_ctrl_p_n_alternate_bindings() -> None:
app = _make_app()
async with app.run_test() as pilot:
await _send_messages(pilot, ["hello", "world"])
# ctrl+p should enter rewind mode
await pilot.press("ctrl+p")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
assert app._rewind_mode is True
assert app._rewind_highlighted_widget is not None
assert app._rewind_highlighted_widget._content == "world"
# ctrl+p again goes to previous
await pilot.press("ctrl+p")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
assert app._rewind_highlighted_widget is not None
assert app._rewind_highlighted_widget._content == "hello"
# ctrl+n goes back
await pilot.press("ctrl+n")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
assert app._rewind_highlighted_widget is not None
assert app._rewind_highlighted_widget._content == "world"
@pytest.mark.asyncio
async def test_rewind_confirm_edits_message_and_prefills_input() -> None:
app = _make_app()
async with app.run_test() as pilot:
await _send_messages(pilot, ["hello", "world"])
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
# Confirm with enter (selects "Edit message from here")
await pilot.press("enter")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.2)
assert app._rewind_mode is False
assert app._current_bottom_app == BottomApp.Input
# Input should be pre-filled with the rewound message
chat_input = app.query_one(ChatInputContainer)
assert chat_input.value == "world"
@pytest.mark.asyncio
async def test_rewind_removes_messages_after_selected() -> None:
app = _make_app()
async with app.run_test() as pilot:
await _send_messages(pilot, ["first", "second", "third"])
# Navigate to "second"
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
assert app._rewind_highlighted_widget is not None
assert app._rewind_highlighted_widget._content == "second"
# Confirm
await pilot.press("enter")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.2)
# Only "first" should remain as a UserMessage
messages_area = app.query_one("#messages")
user_widgets = [
child for child in messages_area.children if isinstance(child, UserMessage)
]
assert len(user_widgets) == 1
assert user_widgets[0]._content == "first"
@pytest.mark.asyncio
async def test_rewind_does_not_activate_while_agent_running() -> None:
app = _make_app()
async with app.run_test() as pilot:
await _send_messages(pilot, ["hello"])
app._agent_running = True
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
assert app._rewind_mode is False
@pytest.mark.asyncio
async def test_rewind_option_selection_with_number_keys() -> None:
app = _make_app()
async with app.run_test() as pilot:
await _send_messages(pilot, ["hello"])
await pilot.press("alt+up")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
# Press "1" to select first option directly
await pilot.press("1")
await pilot.app.workers.wait_for_complete()
await pilot.pause(0.2)
assert app._rewind_mode is False
assert app._current_bottom_app == BottomApp.Input