v2.7.0 (#534)
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>
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
47
tests/cli/textual_ui/windowing/test_session_windowing.py
Normal 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
|
||||
308
tests/core/test_rewind_integration.py
Normal 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"
|
||||
594
tests/core/test_rewind_manager.py
Normal 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]
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="337.2" textLength="146.4" clip-path="url(#terminal-line-13)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="337.2" textLength="122" clip-path="url(#terminal-line-13)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="361.6" textLength="414.8" clip-path="url(#terminal-line-14)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="386" textLength="61" clip-path="url(#terminal-line-15)">Type </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)"> for more 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 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! How can I help 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 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 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 to: third 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 message index: 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)">› 1. Edit & restore files to this 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)">  2. Edit without restoring 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+↑↓ or Ctrl+P/N browse messages  ↑↓ pick option  Enter confirm  ESC 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% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type </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)"> for more 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 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! How can I help 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 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 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)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐</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)">></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% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="337.2" textLength="146.4" clip-path="url(#terminal-line-13)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="337.2" textLength="122" clip-path="url(#terminal-line-13)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="361.6" textLength="414.8" clip-path="url(#terminal-line-14)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="386" textLength="61" clip-path="url(#terminal-line-15)">Type </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)"> for more 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 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! How can I help 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 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 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 to: third 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)">› 1. Edit & restore files to this 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)">  2. Edit without restoring 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+↑↓ or Ctrl+P/N browse messages  ↑↓ pick option  Enter confirm  ESC 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% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="337.2" textLength="146.4" clip-path="url(#terminal-line-13)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="337.2" textLength="122" clip-path="url(#terminal-line-13)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="361.6" textLength="414.8" clip-path="url(#terminal-line-14)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="386" textLength="61" clip-path="url(#terminal-line-15)">Type </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)"> for more 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 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! How can I help 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 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 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 to: second 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)">› 1. Edit & restore files to this 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)">  2. Edit without restoring 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+↑↓ or Ctrl+P/N browse messages  ↑↓ pick option  Enter confirm  ESC 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% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="337.2" textLength="146.4" clip-path="url(#terminal-line-13)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="337.2" textLength="122" clip-path="url(#terminal-line-13)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="361.6" textLength="414.8" clip-path="url(#terminal-line-14)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="386" textLength="61" clip-path="url(#terminal-line-15)">Type </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)"> for more 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 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! How can I help 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 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 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 to: third 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)">› 1. Edit & restore files to this 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)">  2. Edit without restoring 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+↑↓ or Ctrl+P/N browse messages  ↑↓ pick option  Enter confirm  ESC 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% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type </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)"> for more 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, can you help 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! What do you 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 read my config 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 is the content of your config 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)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐</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)">></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% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -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")
|
||||
|
|
|
|||
142
tests/snapshots/test_ui_snapshot_rewind.py
Normal 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,
|
||||
)
|
||||
47
tests/snapshots/test_ui_snapshot_session_resume_injected.py
Normal 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,
|
||||
)
|
||||
|
|
@ -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
|
|
@ -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
|
||||