diff --git a/EvoScientist/EvoScientist.py b/EvoScientist/EvoScientist.py index b440141..b00b739 100644 --- a/EvoScientist/EvoScientist.py +++ b/EvoScientist/EvoScientist.py @@ -257,7 +257,7 @@ def _get_default_backend(): workspace_dir = str(_paths_mod.WORKSPACE_ROOT) set_active_workspace(workspace_dir) - memory_dir = str(_paths_mod.MEMORY_DIR) + memory_dir = str(_paths_mod.MEMORIES_DIR) user_skills_dir = str(_paths_mod.USER_SKILLS_DIR) global_skills_dir = str(_paths_mod.GLOBAL_SKILLS_DIR) @@ -279,7 +279,7 @@ def _get_default_backend(): default=ws_backend, routes={ "/skills/": sk_backend, - "/memory/": mem_backend, + "/memories/": mem_backend, }, ) @@ -296,7 +296,7 @@ def _get_default_middleware(): cfg = _ensure_config() model = _ensure_chat_model() - memory_dir = str(_paths_mod.MEMORY_DIR) + memory_dir = str(_paths_mod.MEMORIES_DIR) mw = [ create_context_editing_middleware(model), ContextOverflowMapperMiddleware(), @@ -395,7 +395,7 @@ def create_cli_agent(workspace_dir: str | None = None, checkpointer=None, config workspace_dir = str(_paths.WORKSPACE_ROOT) # Read paths dynamically so runtime set_workspace_root() changes are picked up - _mem_dir = str(_paths.MEMORY_DIR) + _mem_dir = str(_paths.MEMORIES_DIR) _usr_skills_dir = str(_paths.USER_SKILLS_DIR) _global_skills_dir = str(_paths.GLOBAL_SKILLS_DIR) @@ -412,7 +412,6 @@ def create_cli_agent(workspace_dir: str | None = None, checkpointer=None, config global_dir=_global_skills_dir, secondary_dir=SKILLS_DIR, ) - # Memory always uses SHARED directory (not per-session) for cross-session persistence mem_backend = FilesystemBackend( root_dir=_mem_dir, virtual_mode=True, @@ -421,7 +420,7 @@ def create_cli_agent(workspace_dir: str | None = None, checkpointer=None, config default=ws_backend, routes={ "/skills/": sk_backend, - "/memory/": mem_backend, + "/memories/": mem_backend, }, ) diff --git a/EvoScientist/cli/interactive.py b/EvoScientist/cli/interactive.py index 38feefd..f8cae4e 100644 --- a/EvoScientist/cli/interactive.py +++ b/EvoScientist/cli/interactive.py @@ -294,7 +294,7 @@ def cmd_interactive( from .. import paths - memory_dir = str(paths.MEMORY_DIR) + memory_dir = str(paths.MEMORIES_DIR) from ..config.settings import get_config_dir diff --git a/EvoScientist/cli/mcp_install_cmd.py b/EvoScientist/cli/mcp_install_cmd.py index 3d4e060..a2706a9 100644 --- a/EvoScientist/cli/mcp_install_cmd.py +++ b/EvoScientist/cli/mcp_install_cmd.py @@ -9,7 +9,6 @@ from __future__ import annotations from collections import Counter import questionary -from prompt_toolkit.styles import Style as PtStyle from questionary import Choice from ..mcp.registry import ( @@ -22,17 +21,7 @@ from ..mcp.registry import ( install_mcp_servers, ) from ..stream.display import console - -_PICKER_STYLE = PtStyle.from_dict( - { - "questionmark": "#888888", - "question": "", - "pointer": "bold", - "highlighted": "bold", - "text": "#888888", - "answer": "bold", - } -) +from .interactive import _PICKER_STYLE _INSTALLED_INDICATOR = ("fg:#4caf50", "\u2713 ") diff --git a/EvoScientist/cli/skills_cmd.py b/EvoScientist/cli/skills_cmd.py index 90dcfa3..9f13602 100644 --- a/EvoScientist/cli/skills_cmd.py +++ b/EvoScientist/cli/skills_cmd.py @@ -150,22 +150,11 @@ def _cmd_install_skills(args: str = "") -> None: from collections import Counter import questionary - from prompt_toolkit.styles import Style as PtStyle from questionary import Choice from ..paths import GLOBAL_SKILLS_DIR, USER_SKILLS_DIR from ..tools.skills_manager import fetch_remote_skill_index, install_skill - - _PICKER_STYLE = PtStyle.from_dict( - { - "questionmark": "#888888", - "question": "", - "pointer": "bold", - "highlighted": "bold", - "text": "#888888", - "answer": "bold", - } - ) + from .interactive import _PICKER_STYLE # Installed-item indicator style for disabled checkbox choices. _INSTALLED_INDICATOR = ("fg:#4caf50", "✓ ") diff --git a/EvoScientist/commands/implementation/general.py b/EvoScientist/commands/implementation/general.py index ca19270..42c6b93 100644 --- a/EvoScientist/commands/implementation/general.py +++ b/EvoScientist/commands/implementation/general.py @@ -49,7 +49,7 @@ class CurrentCommand(Command): f"Workspace: {_shorten_path(ctx.workspace_dir)}", style="dim", ) - memory_path = paths.MEMORY_DIR + memory_path = paths.MEMORIES_DIR if memory_path: from ...cli.agent import _shorten_path diff --git a/EvoScientist/middleware/memory.py b/EvoScientist/middleware/memory.py index a220891..9fe8582 100644 --- a/EvoScientist/middleware/memory.py +++ b/EvoScientist/middleware/memory.py @@ -4,7 +4,7 @@ Automatically extracts and persists long-term memory (user profile, research preferences, experiment conclusions) from conversations. Two mechanisms: -1. **Injection** (every LLM call): Reads ``/memory/MEMORY.md`` and appends it +1. **Injection** (every LLM call): Reads ``/memories/MEMORY.md`` and appends it to the system prompt so the agent always has context. 2. **Extraction** (threshold-triggered): When the conversation exceeds a configurable message count, uses an LLM call to pull out structured facts @@ -17,7 +17,7 @@ from EvoScientist.middleware import EvoMemoryMiddleware middleware = EvoMemoryMiddleware( backend=my_backend, # or backend factory - memory_path="/memory/MEMORY.md", + memory_path="/memories/MEMORY.md", extraction_model=chat_model, trigger=("messages", 20), ) @@ -168,7 +168,7 @@ Use this to personalize your responses and avoid re-asking known information. - An experiment completes with notable conclusions **How to update memory:** -- If `/memory/MEMORY.md` does not exist yet, use `write_file` to create it +- If `/memories/MEMORY.md` does not exist yet, use `write_file` to create it - If it already exists, use `edit_file` to update specific sections - Use this markdown structure: @@ -446,7 +446,7 @@ class EvoMemoryMiddleware(AgentMiddleware): Args: backend: Backend instance or factory for reading/writing memory files. - memory_path: Virtual path to MEMORY.md (default ``/memory/MEMORY.md``). + memory_path: Virtual path to MEMORY.md (default ``/memories/MEMORY.md``). extraction_model: Chat model used for extraction (can be a cheap/fast model like ``claude-haiku``). If ``None``, automatic extraction is disabled and only prompt injection + manual ``edit_file`` works. @@ -461,7 +461,7 @@ class EvoMemoryMiddleware(AgentMiddleware): self, *, backend: BACKEND_TYPES, - memory_path: str = "/memory/MEMORY.md", + memory_path: str = "/memories/MEMORY.md", extraction_model: BaseChatModel | None = None, trigger: tuple[str, int] = ("messages", 20), ) -> None: @@ -687,7 +687,7 @@ class EvoMemoryMiddleware(AgentMiddleware): logger.debug("Failed to load memory during modify_request: %s", e) # Use placeholder when memory file doesn't exist yet if not memory_content: - memory_content = "(No memory saved yet. Create `/memory/MEMORY.md` when you learn important information.)" + memory_content = "(No memory saved yet. Create `/memories/MEMORY.md` when you learn important information.)" from deepagents.middleware._utils import append_to_system_message @@ -802,7 +802,7 @@ def create_memory_middleware( """ from deepagents.backends import FilesystemBackend - from ..paths import MEMORY_DIR as _DEFAULT_MEMORY_DIR + from ..paths import MEMORIES_DIR as _DEFAULT_MEMORY_DIR if memory_dir is None: memory_dir = str(_DEFAULT_MEMORY_DIR) diff --git a/EvoScientist/paths.py b/EvoScientist/paths.py index d7e54eb..82d3eab 100644 --- a/EvoScientist/paths.py +++ b/EvoScientist/paths.py @@ -22,7 +22,6 @@ def _env_path(key: str) -> Path | None: WORKSPACE_ROOT = _env_path("EVOSCIENTIST_WORKSPACE_DIR") or Path.cwd() RUNS_DIR = _env_path("EVOSCIENTIST_RUNS_DIR") or (WORKSPACE_ROOT / "runs") -MEMORY_DIR = _env_path("EVOSCIENTIST_MEMORY_DIR") or (WORKSPACE_ROOT / "memory") USER_SKILLS_DIR = _env_path("EVOSCIENTIST_SKILLS_DIR") or (WORKSPACE_ROOT / "skills") MEDIA_DIR = _env_path("EVOSCIENTIST_MEDIA_DIR") or (WORKSPACE_ROOT / "media") @@ -33,9 +32,27 @@ def _global_skills_dir() -> Path: return base / "evoscientist" / "skills" +def _global_memories_dir() -> Path: + xdg = os.environ.get("XDG_CONFIG_HOME") + base = Path(xdg) if xdg else Path.home() / ".config" + return base / "evoscientist" / "memories" + + # Global skills: shared across all workspaces (~/.config/evoscientist/skills/) GLOBAL_SKILLS_DIR: Path = _global_skills_dir() +# Global memories: shared across all workspaces (~/.config/evoscientist/memories/) +GLOBAL_MEMORIES_DIR: Path = _global_memories_dir() + +# Memories dir: global by default, overridable via env var. +# Supports both new (EVOSCIENTIST_MEMORIES_DIR) and old (EVOSCIENTIST_MEMORY_DIR) env vars. +MEMORIES_DIR: Path = ( + _env_path("EVOSCIENTIST_MEMORIES_DIR") + or _env_path("EVOSCIENTIST_MEMORY_DIR") + or GLOBAL_MEMORIES_DIR +) +MEMORY_DIR = MEMORIES_DIR # backward compat alias + def set_workspace_root(path: str | Path) -> None: """Update workspace root and re-derive dependent directories. @@ -43,10 +60,14 @@ def set_workspace_root(path: str | Path) -> None: Directories with an explicit environment-variable override keep their env-var value; all others are re-derived from the new root. Also resets ``_active_workspace`` to the new root as a safe default. + + Note: MEMORIES_DIR is global (not workspace-scoped) but env var overrides + are re-evaluated here to support late-set environment variables. """ global \ WORKSPACE_ROOT, \ RUNS_DIR, \ + MEMORIES_DIR, \ MEMORY_DIR, \ USER_SKILLS_DIR, \ MEDIA_DIR, \ @@ -54,7 +75,12 @@ def set_workspace_root(path: str | Path) -> None: WORKSPACE_ROOT = Path(path).resolve() _active_workspace = WORKSPACE_ROOT RUNS_DIR = _env_path("EVOSCIENTIST_RUNS_DIR") or (WORKSPACE_ROOT / "runs") - MEMORY_DIR = _env_path("EVOSCIENTIST_MEMORY_DIR") or (WORKSPACE_ROOT / "memory") + MEMORIES_DIR = ( + _env_path("EVOSCIENTIST_MEMORIES_DIR") + or _env_path("EVOSCIENTIST_MEMORY_DIR") + or GLOBAL_MEMORIES_DIR + ) + MEMORY_DIR = MEMORIES_DIR USER_SKILLS_DIR = _env_path("EVOSCIENTIST_SKILLS_DIR") or ( WORKSPACE_ROOT / "skills" ) @@ -64,13 +90,13 @@ def set_workspace_root(path: str | Path) -> None: def ensure_dirs() -> None: """Create runtime subdirectories if they do not exist. - Only memory is created eagerly — skills directories are created on demand + Only memories is created eagerly — skills directories are created on demand by install_skill() when the user first installs a skill. Does NOT create the workspace root itself — it should already exist (either the user's cwd or a directory they specified). """ - MEMORY_DIR.mkdir(parents=True, exist_ok=True) + MEMORIES_DIR.mkdir(parents=True, exist_ok=True) def default_workspace_dir() -> Path: diff --git a/EvoScientist/stream/display.py b/EvoScientist/stream/display.py index 56f47bb..ba9bf14 100644 --- a/EvoScientist/stream/display.py +++ b/EvoScientist/stream/display.py @@ -987,16 +987,20 @@ def _get_event_loop() -> asyncio.AbstractEventLoop: def _resolve_ask_user_prompt(ask_user_data: dict) -> dict: """Interactive console Q&A for ask_user events. - Presents questions via ``prompt_toolkit.prompt()`` (not ``input()``) - for proper CJK IME support and styled prompts without cursor drift. + Presents multiple-choice questions with arrow-key navigation via + ``questionary.select()`` and free-text questions via + ``questionary.text()`` with required-field validation. Matches the + questionary style used throughout the rest of the CLI. """ - from prompt_toolkit import prompt as pt_prompt # type: ignore[import-untyped] - from prompt_toolkit.formatted_text import HTML # type: ignore[import-untyped] + import questionary # type: ignore[import-untyped] + + from ..cli.interactive import _PICKER_STYLE questions = ask_user_data.get("questions", []) if not questions: return {"answers": [], "status": "answered"} + total = len(questions) console.print() console.print( Panel( @@ -1013,43 +1017,65 @@ def _resolve_ask_user_prompt(ask_user_data: dict) -> dict: q_text = q.get("question", "") q_type = q.get("type", "text") required = q.get("required", True) - tag = " [dim](optional)[/dim]" if not required else "" - console.print(f" [bold]{i + 1}. {q_text}[/bold]{tag}") + optional_suffix = " (optional)" if not required else "" + prompt_text = f"({i + 1}/{total}) {q_text}{optional_suffix}" + + def _make_validator(is_required: bool): + def _validate(v: str) -> bool | str: + if is_required and not v.strip(): + return "This field is required." + return True + + return _validate if q_type == "multiple_choice": choices = q.get("choices", []) - for j, choice in enumerate(choices): - label = choice.get("value", str(choice)) - letter = chr(ord("A") + j) - console.print(Text(f" {letter}. {label}", style="dim")) - other_letter = chr(ord("A") + len(choices)) - console.print( - Text(f" {other_letter}. Other (type your answer)", style="dim") - ) + choice_labels = [c.get("value", str(c)) for c in choices] + skip_label = "Skip" + if not required: + choice_labels.append(skip_label) + other_label = "Other (type your answer)" + choice_labels.append(other_label) + + selected = questionary.select( + prompt_text, + choices=choice_labels, + style=_PICKER_STYLE, + ).ask() + + if selected is None: # Ctrl+C + raise KeyboardInterrupt + + if selected == skip_label: + answers.append("") + console.print() + continue + + if selected == other_label: + selected = questionary.text( + "Your answer:", + validate=_make_validator(required), + style=_PICKER_STYLE, + ).ask() + if selected is None: + raise KeyboardInterrupt + + answers.append(selected) - letters = "/".join(chr(ord("A") + k) for k in range(len(choices) + 1)) - raw = pt_prompt( - HTML(f" ") - ).strip() - if raw.upper() == other_letter: - raw = pt_prompt( - HTML(" ") - ).strip() - answers.append(raw) - elif len(raw) == 1 and raw.upper().isalpha(): - idx = ord(raw.upper()) - ord("A") - if 0 <= idx < len(choices): - answers.append(choices[idx].get("value", raw)) - else: - answers.append(raw) - else: - answers.append(raw) else: - raw = pt_prompt( - HTML(" ") - ).strip() - answers.append(raw) + answer = questionary.text( + prompt_text, + validate=_make_validator(required), + style=_PICKER_STYLE, + ).ask() + + if answer is None: # Ctrl+C + raise KeyboardInterrupt + + answers.append(answer) + console.print() + except (EOFError, KeyboardInterrupt): console.print("[dim] Cancelled.[/dim]") return {"status": "cancelled"} diff --git a/EvoScientist/stream/utils.py b/EvoScientist/stream/utils.py index 526b899..5cd93d5 100644 --- a/EvoScientist/stream/utils.py +++ b/EvoScientist/stream/utils.py @@ -114,9 +114,9 @@ def _tool_path_arg(args: dict | None) -> str: def _is_memory_path(path: str) -> bool: - """Return True when a virtual path targets the shared memory directory.""" + """Return True when a virtual path targets the global memories directory.""" normalized = (path or "").strip() - return normalized == "/memory" or normalized.startswith("/memory/") + return normalized == "/memories" or normalized.startswith("/memories/") def format_tool_compact(name: str, args: dict | None) -> str: @@ -250,7 +250,7 @@ def format_tool_compact_with_result( if name_lower in ("write_file", "edit_file"): if ( - "/memory/" in result_content + "/memories/" in result_content or "/MEMORY.md" in result_content or "MEMORY.md" in result_content ): diff --git a/tests/test_ask_user.py b/tests/test_ask_user.py index f5d1cb2..90050b2 100644 --- a/tests/test_ask_user.py +++ b/tests/test_ask_user.py @@ -445,30 +445,37 @@ def test_auto_mode_disables_ask_user_middleware( class TestRichCLIPrompt: - """Test _resolve_ask_user_prompt with mocked prompt_toolkit.prompt().""" - - _PT_PROMPT = "prompt_toolkit.prompt" + """Test _resolve_ask_user_prompt with mocked questionary.""" def test_text_question_returns_answered(self): + from unittest.mock import MagicMock + from EvoScientist.stream.display import _resolve_ask_user_prompt data = { "questions": [{"question": "What dataset?", "type": "text"}], "tool_call_id": "tc_1", } - with patch(self._PT_PROMPT, side_effect=["CIFAR-10"]): + mock_text = MagicMock() + mock_text.return_value.ask.return_value = "CIFAR-10" + with patch("questionary.text", mock_text): result = _resolve_ask_user_prompt(data) assert result["status"] == "answered" assert result["answers"] == ["CIFAR-10"] def test_keyboard_interrupt_returns_cancelled(self): + from unittest.mock import MagicMock + from EvoScientist.stream.display import _resolve_ask_user_prompt data = { "questions": [{"question": "What?", "type": "text"}], "tool_call_id": "tc_1", } - with patch(self._PT_PROMPT, side_effect=KeyboardInterrupt): + # questionary returns None when user presses Ctrl+C + mock_text = MagicMock() + mock_text.return_value.ask.return_value = None + with patch("questionary.text", mock_text): result = _resolve_ask_user_prompt(data) assert result["status"] == "cancelled" @@ -480,7 +487,9 @@ class TestRichCLIPrompt: assert result["status"] == "answered" assert result["answers"] == [] - def test_multiple_choice_letter_mapping(self): + def test_multiple_choice_selection(self): + from unittest.mock import MagicMock + from EvoScientist.stream.display import _resolve_ask_user_prompt data = { @@ -493,11 +502,40 @@ class TestRichCLIPrompt: ], "tool_call_id": "tc_1", } - with patch(self._PT_PROMPT, side_effect=["B"]): + mock_select = MagicMock() + mock_select.return_value.ask.return_value = "ImageNet" + with patch("questionary.select", mock_select): result = _resolve_ask_user_prompt(data) assert result["status"] == "answered" assert result["answers"] == ["ImageNet"] + def test_multiple_choice_other_option(self): + from unittest.mock import MagicMock + + from EvoScientist.stream.display import _resolve_ask_user_prompt + + data = { + "questions": [ + { + "question": "Which?", + "type": "multiple_choice", + "choices": [{"value": "CIFAR-10"}, {"value": "ImageNet"}], + } + ], + "tool_call_id": "tc_1", + } + mock_select = MagicMock() + mock_select.return_value.ask.return_value = "Other (type your answer)" + mock_text = MagicMock() + mock_text.return_value.ask.return_value = "custom dataset" + with ( + patch("questionary.select", mock_select), + patch("questionary.text", mock_text), + ): + result = _resolve_ask_user_prompt(data) + assert result["status"] == "answered" + assert result["answers"] == ["custom dataset"] + # --------------------------------------------------------------------------- # TUI widget (basic construction) diff --git a/tests/test_paths.py b/tests/test_paths.py index d044e62..29c0391 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -14,6 +14,7 @@ def _restore_paths(): orig = { "WORKSPACE_ROOT": paths.WORKSPACE_ROOT, "RUNS_DIR": paths.RUNS_DIR, + "MEMORIES_DIR": paths.MEMORIES_DIR, "MEMORY_DIR": paths.MEMORY_DIR, "USER_SKILLS_DIR": paths.USER_SKILLS_DIR, "_active_workspace": paths._active_workspace, @@ -21,6 +22,7 @@ def _restore_paths(): yield paths.WORKSPACE_ROOT = orig["WORKSPACE_ROOT"] paths.RUNS_DIR = orig["RUNS_DIR"] + paths.MEMORIES_DIR = orig["MEMORIES_DIR"] paths.MEMORY_DIR = orig["MEMORY_DIR"] paths.USER_SKILLS_DIR = orig["USER_SKILLS_DIR"] paths._active_workspace = orig["_active_workspace"] @@ -29,8 +31,13 @@ def _restore_paths(): class TestSetWorkspaceRoot: """Tests for set_workspace_root().""" - def test_updates_derived_dirs(self, tmp_path): + def test_updates_derived_dirs(self, tmp_path, monkeypatch): """set_workspace_root should update WORKSPACE_ROOT and all derived dirs.""" + monkeypatch.delenv("EVOSCIENTIST_MEMORIES_DIR", raising=False) + monkeypatch.delenv("EVOSCIENTIST_MEMORY_DIR", raising=False) + monkeypatch.delenv("EVOSCIENTIST_RUNS_DIR", raising=False) + monkeypatch.delenv("EVOSCIENTIST_SKILLS_DIR", raising=False) + new_root = tmp_path / "my_workspace" new_root.mkdir() @@ -38,7 +45,8 @@ class TestSetWorkspaceRoot: assert paths.WORKSPACE_ROOT == new_root.resolve() assert paths.RUNS_DIR == new_root.resolve() / "runs" - assert paths.MEMORY_DIR == new_root.resolve() / "memory" + # MEMORIES_DIR is global — not derived from workspace root + assert paths.MEMORIES_DIR == paths.GLOBAL_MEMORIES_DIR assert paths.USER_SKILLS_DIR == new_root.resolve() / "skills" def test_resets_active_workspace(self, tmp_path): @@ -60,7 +68,7 @@ class TestSetWorkspaceRoot: custom_runs = tmp_path / "custom_runs" env = { - "EVOSCIENTIST_MEMORY_DIR": str(custom_mem), + "EVOSCIENTIST_MEMORIES_DIR": str(custom_mem), "EVOSCIENTIST_SKILLS_DIR": str(custom_skills), "EVOSCIENTIST_RUNS_DIR": str(custom_runs), } @@ -75,8 +83,8 @@ class TestSetWorkspaceRoot: assert paths.WORKSPACE_ROOT == new_root.resolve() assert paths._active_workspace == new_root.resolve() - # Derived dirs should reflect the env overrides, not the new root - assert paths.MEMORY_DIR == custom_mem.expanduser() + # MEMORIES_DIR respects env override + assert paths.MEMORIES_DIR == custom_mem.expanduser() assert paths.USER_SKILLS_DIR == custom_skills.expanduser() assert paths.RUNS_DIR == custom_runs.expanduser() @@ -94,14 +102,21 @@ class TestEnsureDirsUsesUpdatedPaths: """ensure_dirs should create dirs at the currently set paths.""" def test_ensure_dirs_uses_updated_paths(self, tmp_path): - """After set_workspace_root, ensure_dirs creates dirs at new location.""" + """ensure_dirs creates the global memories dir (not workspace-local).""" new_root = tmp_path / "workspace" new_root.mkdir() - paths.set_workspace_root(new_root) - paths.ensure_dirs() + custom_mem = tmp_path / "memories" - assert (new_root / "memory").is_dir() - assert not ( - new_root / "skills" - ).exists() # skills created on demand by install_skill() + with mock.patch.dict( + os.environ, {"EVOSCIENTIST_MEMORIES_DIR": str(custom_mem)} + ): + paths.set_workspace_root(new_root) + paths.ensure_dirs() + + assert custom_mem.is_dir() + assert not (new_root / "memory").exists() # no longer workspace-local + assert not (new_root / "memories").exists() + assert not ( + new_root / "skills" + ).exists() # skills created on demand by install_skill() diff --git a/tests/test_stream_utils.py b/tests/test_stream_utils.py index e009033..16628fc 100644 --- a/tests/test_stream_utils.py +++ b/tests/test_stream_utils.py @@ -95,38 +95,51 @@ class TestFormatToolCompact: result = format_tool_compact("edit_file", {"path": "f.py"}) assert result == "edit_file(f.py)" - # Memory file special display - def test_read_file_memory(self): - result = format_tool_compact("read_file", {"path": "/memory/MEMORY.md"}) + # Global memory file special display (/memories/ = global) + def test_read_file_global_memory(self): + result = format_tool_compact("read_file", {"path": "/memories/MEMORY.md"}) assert result == "Reading memory" - def test_read_file_memory_file_path_alias(self): - result = format_tool_compact("read_file", {"file_path": "/memory/MEMORY.md"}) + def test_read_file_global_memory_file_path_alias(self): + result = format_tool_compact("read_file", {"file_path": "/memories/MEMORY.md"}) assert result == "Reading memory" - def test_read_file_any_memory_file(self): - result = format_tool_compact("read_file", {"path": "/memory/history.md"}) + def test_read_file_any_global_memory_file(self): + result = format_tool_compact("read_file", {"path": "/memories/history.md"}) assert result == "Reading memory" - def test_write_file_memory(self): + def test_write_file_global_memory(self): result = format_tool_compact("write_file", {"path": "/MEMORY.md"}) assert result == "Updating memory" - # Also covers paths with /memory/ prefix - result2 = format_tool_compact("write_file", {"path": "/memory/MEMORY.md"}) + # Also covers paths with /memories/ prefix + result2 = format_tool_compact("write_file", {"path": "/memories/MEMORY.md"}) assert result2 == "Updating memory" - def test_edit_file_memory(self): - result = format_tool_compact("edit_file", {"path": "/memory/MEMORY.md"}) + def test_edit_file_global_memory(self): + result = format_tool_compact("edit_file", {"path": "/memories/MEMORY.md"}) assert result == "Updating memory" - def test_write_edit_any_memory_file(self): - write_result = format_tool_compact("write_file", {"path": "/memory/soul.md"}) + def test_write_edit_any_global_memory_file(self): + write_result = format_tool_compact("write_file", {"path": "/memories/soul.md"}) edit_result = format_tool_compact( - "edit_file", {"path": "/memory/skills-context.md"} + "edit_file", {"path": "/memories/skills-context.md"} ) assert write_result == "Updating memory" assert edit_result == "Updating memory" + # Project-local /memory/ files show normal tool display + def test_read_file_project_memory(self): + result = format_tool_compact( + "read_file", {"path": "/memory/ideation-memory.md"} + ) + assert result == "read_file(/memory/ideation-memory.md)" + + def test_edit_file_project_memory(self): + result = format_tool_compact( + "edit_file", {"path": "/memory/experiment-memory.md"} + ) + assert result == "edit_file(/memory/experiment-memory.md)" + def test_memory_display_inferred_from_result_when_args_sparse(self): read_result = format_tool_compact_with_result( "read_file", @@ -138,17 +151,25 @@ class TestFormatToolCompact: edit_result = format_tool_compact_with_result( "edit_file", {}, - "Successfully replaced 1 instance(s) of the string in '/memory/MEMORY.md'", + "Successfully replaced 1 instance(s) of the string in '/memories/MEMORY.md'", ) assert edit_result == "Updating memory" write_result = format_tool_compact_with_result( "write_file", {}, - "Wrote updated content to '/memory/history.md'", + "Wrote updated content to '/memories/history.md'", ) assert write_result == "Updating memory" + def test_project_memory_result_not_special(self): + result = format_tool_compact_with_result( + "write_file", + {}, + "Wrote updated content to '/memory/ideation-memory.md'", + ) + assert result != "Updating memory" + def test_glob(self): result = format_tool_compact("glob", {"pattern": "*.py"}) assert result == "glob(*.py)" diff --git a/tests/test_tui_widgets.py b/tests/test_tui_widgets.py index 15dc384..eff270a 100644 --- a/tests/test_tui_widgets.py +++ b/tests/test_tui_widgets.py @@ -206,7 +206,7 @@ class TestToolCallWidget(unittest.TestCase): w = ToolCallWidget("edit_file", {}, "mem-1") w._result_content = ( - "Successfully replaced 1 instance(s) of the string in '/memory/MEMORY.md'" + "Successfully replaced 1 instance(s) of the string in '/memories/MEMORY.md'" ) class _Header: