From 5554c45b9c937a01067834c6c54e597d42b4a750 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 26 Apr 2026 00:28:28 -0700 Subject: [PATCH 01/25] feat: Save and load MCP, tool, and skill states in sessions Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/commands/load_session.py | 2 +- cecli/main.py | 2 +- cecli/sessions.py | 68 ++++++++++++++++++++++++++++++++-- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/cecli/commands/load_session.py b/cecli/commands/load_session.py index 1d5676d97e9..f3c38396a8b 100644 --- a/cecli/commands/load_session.py +++ b/cecli/commands/load_session.py @@ -18,7 +18,7 @@ async def execute(cls, io, coder, args, **kwargs): from cecli import sessions session_manager = sessions.SessionManager(coder, io) - session_manager.load_session(args.strip()) + await session_manager.load_session(args.strip()) return format_command_result(io, "load-session", f"Loaded session: {args.strip()}") diff --git a/cecli/main.py b/cecli/main.py index 76b92cb0870..077fb114b34 100644 --- a/cecli/main.py +++ b/cecli/main.py @@ -1301,7 +1301,7 @@ def apply_model_overrides(model_name): from cecli.sessions import SessionManager session_manager = SessionManager(coder, io) - session_manager.load_session( + await session_manager.load_session( args.auto_save_session_name if args.auto_save_session_name else "auto-save" ) except Exception: diff --git a/cecli/sessions.py b/cecli/sessions.py index 5d8447d5213..cb361810696 100644 --- a/cecli/sessions.py +++ b/cecli/sessions.py @@ -7,6 +7,7 @@ from cecli import models from cecli.helpers.conversation import ConversationService, MessageTag +from cecli.hooks.manager import HookManager class SessionManager: @@ -88,7 +89,7 @@ def list_sessions(self) -> List[Dict]: return sessions - def load_session(self, session_identifier: str) -> bool: + async def load_session(self, session_identifier: str) -> bool: """Load a saved session by name or file path.""" if not session_identifier: self.io.tool_error("Please provide a session name or file path.") @@ -112,7 +113,17 @@ def load_session(self, session_identifier: str) -> bool: return False # Apply session data - return self._apply_session_data(session_data, session_file) + applied = await self._apply_session_data(session_data, session_file) + if applied: + from cecli.commands import SwitchCoderSignal + + raise SwitchCoderSignal( + edit_format=self.coder.edit_format, + from_coder=self.coder, + summarize_from_coder=False, + show_announcements=True, + ) + return applied def _build_session_data(self, session_name) -> Dict: """Build session data dictionary from current coder state.""" @@ -140,6 +151,20 @@ def _build_session_data(self, session_name) -> Dict: self.io.tool_warning(f"Could not read todo list file: {e}") # Get CUR and DONE messages from ConversationManager + connected_mcps = [] + if hasattr(self.coder, "mcp_manager") and self.coder.mcp_manager: + connected_mcps = [server.name for server in self.coder.mcp_manager.connected_servers] + + hook_manager = HookManager() + enabled_hooks = [hook.name for hook in hook_manager.get_hooks()] + + skills_data = None + if hasattr(self.coder, "skills_manager") and self.coder.skills_manager: + skills_data = { + "include": self.coder.skills_manager.include_list, + "exclude": self.coder.skills_manager.exclude_list, + } + return { "version": 1, "session_name": session_name, @@ -168,6 +193,9 @@ def _build_session_data(self, session_name) -> Dict: "auto_test": self.coder.auto_test, }, "todo_list": todo_content, + "mcps": connected_mcps, + "hooks": enabled_hooks, + "skills": skills_data, } def _find_session_file(self, session_identifier: str) -> Optional[Path]: @@ -194,7 +222,7 @@ def _find_session_file(self, session_identifier: str) -> Optional[Path]: self.io.tool_output("Use /list-sessions to see available sessions.") return None - def _apply_session_data(self, session_data: Dict, session_file: Path) -> bool: + async def _apply_session_data(self, session_data: Dict, session_file: Path) -> bool: """Apply session data to current coder state.""" try: # Clear current state @@ -303,6 +331,40 @@ def _apply_session_data(self, session_data: Dict, session_file: Path) -> bool: ) self.io.tool_output(f"Loaded {num_messages} messages and {num_files} files") + # Load MCPs + saved_mcps = session_data.get("mcps", []) + if hasattr(self.coder, "mcp_manager") and self.coder.mcp_manager: + current_mcps = {server.name for server in self.coder.mcp_manager.connected_servers} + saved_mcps_set = set(saved_mcps) + + to_disconnect = current_mcps - saved_mcps_set + for mcp_name in to_disconnect: + await self.coder.mcp_manager.disconnect_server(mcp_name) + + to_connect = saved_mcps_set - current_mcps + for mcp_name in to_connect: + await self.coder.mcp_manager.connect_server(mcp_name) + + # Load hooks + hook_manager = HookManager() + saved_hooks = session_data.get("hooks", []) + # Disable all hooks first + for hook in hook_manager.get_hooks(): + hook_manager.disable_hook(hook.name) + # Enable saved hooks + for hook_name in saved_hooks: + hook_manager.enable_hook(hook_name) + + # Load skills + skills_data = session_data.get("skills") + if ( + skills_data + and hasattr(self.coder, "skills_manager") + and self.coder.skills_manager + ): + self.coder.skills_manager.include_list = skills_data.get("include", []) + self.coder.skills_manager.exclude_list = skills_data.get("exclude", []) + return True except Exception as e: From 9213b2f026dbc6ab7d1b7e62f0ac77d8e8cc43e8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 26 Apr 2026 14:46:20 -0700 Subject: [PATCH 02/25] fix: Correctly retrieve and manage enabled hooks during session save/load Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/sessions.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cecli/sessions.py b/cecli/sessions.py index cb361810696..8bb650b18f5 100644 --- a/cecli/sessions.py +++ b/cecli/sessions.py @@ -156,7 +156,11 @@ def _build_session_data(self, session_name) -> Dict: connected_mcps = [server.name for server in self.coder.mcp_manager.connected_servers] hook_manager = HookManager() - enabled_hooks = [hook.name for hook in hook_manager.get_hooks()] + all_hooks_by_type = hook_manager.get_all_hooks() + enabled_hooks = [] + for hook_type in all_hooks_by_type: + for hook in hook_manager.get_hooks(hook_type): + enabled_hooks.append(hook.name) skills_data = None if hasattr(self.coder, "skills_manager") and self.coder.skills_manager: @@ -349,8 +353,10 @@ async def _apply_session_data(self, session_data: Dict, session_file: Path) -> b hook_manager = HookManager() saved_hooks = session_data.get("hooks", []) # Disable all hooks first - for hook in hook_manager.get_hooks(): - hook_manager.disable_hook(hook.name) + all_hooks_by_type = hook_manager.get_all_hooks() + for hook_type in all_hooks_by_type: + for hook in hook_manager.get_hooks(hook_type): + hook_manager.disable_hook(hook.name) # Enable saved hooks for hook_name in saved_hooks: hook_manager.enable_hook(hook_name) From 999c121e624656ead9bea4a0caaf0a0f32eca55f Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 26 Apr 2026 21:09:04 -0700 Subject: [PATCH 03/25] refactor: Save and load agent tools configuration in session Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/sessions.py | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/cecli/sessions.py b/cecli/sessions.py index 8bb650b18f5..5e9ddbc4c11 100644 --- a/cecli/sessions.py +++ b/cecli/sessions.py @@ -7,7 +7,6 @@ from cecli import models from cecli.helpers.conversation import ConversationService, MessageTag -from cecli.hooks.manager import HookManager class SessionManager: @@ -155,12 +154,10 @@ def _build_session_data(self, session_name) -> Dict: if hasattr(self.coder, "mcp_manager") and self.coder.mcp_manager: connected_mcps = [server.name for server in self.coder.mcp_manager.connected_servers] - hook_manager = HookManager() - all_hooks_by_type = hook_manager.get_all_hooks() - enabled_hooks = [] - for hook_type in all_hooks_by_type: - for hook in hook_manager.get_hooks(hook_type): - enabled_hooks.append(hook.name) + # Get CUR and DONE messages from ConversationManager + connected_mcps = [] + if hasattr(self.coder, "mcp_manager") and self.coder.mcp_manager: + connected_mcps = [server.name for server in self.coder.mcp_manager.connected_servers] skills_data = None if hasattr(self.coder, "skills_manager") and self.coder.skills_manager: @@ -169,6 +166,14 @@ def _build_session_data(self, session_name) -> Dict: "exclude": self.coder.skills_manager.exclude_list, } + agent_config_data = None + if hasattr(self.coder, "agent_config"): + agent_config_data = { + "tools_paths": self.coder.agent_config.get("tools_paths", []), + "tools_includelist": self.coder.agent_config.get("tools_includelist", []), + "tools_excludelist": self.coder.agent_config.get("tools_excludelist", []), + } + return { "version": 1, "session_name": session_name, @@ -198,8 +203,8 @@ def _build_session_data(self, session_name) -> Dict: }, "todo_list": todo_content, "mcps": connected_mcps, - "hooks": enabled_hooks, "skills": skills_data, + "agent_config": agent_config_data, } def _find_session_file(self, session_identifier: str) -> Optional[Path]: @@ -349,18 +354,6 @@ async def _apply_session_data(self, session_data: Dict, session_file: Path) -> b for mcp_name in to_connect: await self.coder.mcp_manager.connect_server(mcp_name) - # Load hooks - hook_manager = HookManager() - saved_hooks = session_data.get("hooks", []) - # Disable all hooks first - all_hooks_by_type = hook_manager.get_all_hooks() - for hook_type in all_hooks_by_type: - for hook in hook_manager.get_hooks(hook_type): - hook_manager.disable_hook(hook.name) - # Enable saved hooks - for hook_name in saved_hooks: - hook_manager.enable_hook(hook_name) - # Load skills skills_data = session_data.get("skills") if ( @@ -371,6 +364,15 @@ async def _apply_session_data(self, session_data: Dict, session_file: Path) -> b self.coder.skills_manager.include_list = skills_data.get("include", []) self.coder.skills_manager.exclude_list = skills_data.get("exclude", []) + # Load agent_config for tools + agent_config_data = session_data.get("agent_config") + if agent_config_data and hasattr(self.coder, "agent_config"): + self.coder.agent_config.update(agent_config_data) + from cecli.tools.utils.registry import ToolRegistry + + ToolRegistry.build_registry(agent_config=self.coder.agent_config) + self.coder.loaded_custom_tools = ToolRegistry.loaded_custom_tools + return True except Exception as e: From 25294e8432d31d6e94d394947d3b9c5d0af910ea Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 27 Apr 2026 11:05:45 -0700 Subject: [PATCH 04/25] fix: Convert skill lists to serializable format for session saving Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/sessions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cecli/sessions.py b/cecli/sessions.py index 5e9ddbc4c11..d2a6e785d1e 100644 --- a/cecli/sessions.py +++ b/cecli/sessions.py @@ -162,8 +162,8 @@ def _build_session_data(self, session_name) -> Dict: skills_data = None if hasattr(self.coder, "skills_manager") and self.coder.skills_manager: skills_data = { - "include": self.coder.skills_manager.include_list, - "exclude": self.coder.skills_manager.exclude_list, + "include": list(self.coder.skills_manager.include_list), + "exclude": list(self.coder.skills_manager.exclude_list), } agent_config_data = None @@ -361,8 +361,8 @@ async def _apply_session_data(self, session_data: Dict, session_file: Path) -> b and hasattr(self.coder, "skills_manager") and self.coder.skills_manager ): - self.coder.skills_manager.include_list = skills_data.get("include", []) - self.coder.skills_manager.exclude_list = skills_data.get("exclude", []) + self.coder.skills_manager.include_list = set(skills_data.get("include", [])) + self.coder.skills_manager.exclude_list = set(skills_data.get("exclude", [])) # Load agent_config for tools agent_config_data = session_data.get("agent_config") From a3ac3b50b327a5748d6f32d79b32d3e7e4923a48 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 27 Apr 2026 13:58:14 -0700 Subject: [PATCH 05/25] fix: Handle None skill lists when saving sessions Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/sessions.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cecli/sessions.py b/cecli/sessions.py index d2a6e785d1e..1d9fa5177c4 100644 --- a/cecli/sessions.py +++ b/cecli/sessions.py @@ -162,8 +162,12 @@ def _build_session_data(self, session_name) -> Dict: skills_data = None if hasattr(self.coder, "skills_manager") and self.coder.skills_manager: skills_data = { - "include": list(self.coder.skills_manager.include_list), - "exclude": list(self.coder.skills_manager.exclude_list), + "include": list(self.coder.skills_manager.include_list) + if self.coder.skills_manager.include_list is not None + else [], + "exclude": list(self.coder.skills_manager.exclude_list) + if self.coder.skills_manager.exclude_list is not None + else [], } agent_config_data = None From 59789b52f8baf427aa201f2e1bc13b2e81478ff5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 27 Apr 2026 16:25:58 -0700 Subject: [PATCH 06/25] refactor: Update session to save skills paths and lists Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/sessions.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cecli/sessions.py b/cecli/sessions.py index 1d9fa5177c4..5469fa76fa2 100644 --- a/cecli/sessions.py +++ b/cecli/sessions.py @@ -162,10 +162,11 @@ def _build_session_data(self, session_name) -> Dict: skills_data = None if hasattr(self.coder, "skills_manager") and self.coder.skills_manager: skills_data = { - "include": list(self.coder.skills_manager.include_list) + "skills_paths": self.coder.skills_manager.directory_paths, + "skills_includelist": list(self.coder.skills_manager.include_list) if self.coder.skills_manager.include_list is not None else [], - "exclude": list(self.coder.skills_manager.exclude_list) + "skills_excludelist": list(self.coder.skills_manager.exclude_list) if self.coder.skills_manager.exclude_list is not None else [], } @@ -365,8 +366,13 @@ async def _apply_session_data(self, session_data: Dict, session_file: Path) -> b and hasattr(self.coder, "skills_manager") and self.coder.skills_manager ): - self.coder.skills_manager.include_list = set(skills_data.get("include", [])) - self.coder.skills_manager.exclude_list = set(skills_data.get("exclude", [])) + self.coder.skills_manager.directory_paths = skills_data.get("skills_paths", []) + self.coder.skills_manager.include_list = set( + skills_data.get("skills_includelist", []) + ) + self.coder.skills_manager.exclude_list = set( + skills_data.get("skills_excludelist", []) + ) # Load agent_config for tools agent_config_data = session_data.get("agent_config") From a435f07162bd20078238767fef322c50d271fc95 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 27 Apr 2026 18:14:15 -0700 Subject: [PATCH 07/25] fix: Convert WindowsPath to string for JSON serialization Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/sessions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cecli/sessions.py b/cecli/sessions.py index 5469fa76fa2..f31897cd562 100644 --- a/cecli/sessions.py +++ b/cecli/sessions.py @@ -162,7 +162,7 @@ def _build_session_data(self, session_name) -> Dict: skills_data = None if hasattr(self.coder, "skills_manager") and self.coder.skills_manager: skills_data = { - "skills_paths": self.coder.skills_manager.directory_paths, + "skills_paths": [str(p) for p in self.coder.skills_manager.directory_paths], "skills_includelist": list(self.coder.skills_manager.include_list) if self.coder.skills_manager.include_list is not None else [], From 6e4f9eb5d828a774dd3f4cda73f09a19aeda691c Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 27 Apr 2026 20:07:56 -0700 Subject: [PATCH 08/25] fix: Rename agent_config to tools in session data Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/sessions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cecli/sessions.py b/cecli/sessions.py index f31897cd562..17489c9f623 100644 --- a/cecli/sessions.py +++ b/cecli/sessions.py @@ -209,7 +209,7 @@ def _build_session_data(self, session_name) -> Dict: "todo_list": todo_content, "mcps": connected_mcps, "skills": skills_data, - "agent_config": agent_config_data, + "tools": agent_config_data, } def _find_session_file(self, session_identifier: str) -> Optional[Path]: @@ -374,8 +374,8 @@ async def _apply_session_data(self, session_data: Dict, session_file: Path) -> b skills_data.get("skills_excludelist", []) ) - # Load agent_config for tools - agent_config_data = session_data.get("agent_config") + # Load tools config + agent_config_data = session_data.get("tools") if agent_config_data and hasattr(self.coder, "agent_config"): self.coder.agent_config.update(agent_config_data) from cecli.tools.utils.registry import ToolRegistry From d7b6d3b1d6440ed388a2e0a645d4a5b39f55d6bb Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 27 Apr 2026 20:19:43 -0700 Subject: [PATCH 09/25] test: add tests for session save/load agent config Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- tests/basic/test_sessions.py | 115 ++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/tests/basic/test_sessions.py b/tests/basic/test_sessions.py index adb5a01a907..aafc8105756 100644 --- a/tests/basic/test_sessions.py +++ b/tests/basic/test_sessions.py @@ -7,7 +7,7 @@ from unittest import TestCase, mock from cecli.coders import Coder -from cecli.commands import Commands +from cecli.commands import Commands, SwitchCoderSignal from cecli.helpers.file_searcher import handle_core_files from cecli.io import InputOutput from cecli.models import Model @@ -196,3 +196,116 @@ async def test_preserve_todo_list_deprecated(self): self.assertTrue( any("deprecated" in call[0][0] for call in mock_tool_warning.call_args_list) ) + + async def test_cmd_save_load_session_agent_config(self): + """Test session save/load for agent-specific configs (mcp, skills, tools).""" + with GitTemporaryDirectory(): + # Mock args for AgentCoder + mock_args = mock.MagicMock() + mock_args.agent_config = json.dumps( + { + "tools_paths": ["/test/tools/path"], + "tools_includelist": ["included_tool"], + "tools_excludelist": ["excluded_tool"], + } + ) + # This is needed for the skills manager to be created + mock_args.skills_paths = ["/test/skills/path"] + mock_args.mcp_servers = json.dumps([{"name": "mock_mcp"}]) + mock_args.mcp_servers_files = [] + mock_args.verbose = False + mock_args.debug = False + mock_args.tui = False + mock_args.auto_save_session_name = "auto-save" + mock_args.auto_save = False + mock_args.auto_load = False + mock_args.yes_always_commands = True + mock_args.command_prefix = None + mock_args.file_diffs = True + mock_args.max_reflections = 3 + mock_args.model = "gpt-3.5-turbo" + mock_args.weak_model = None + mock_args.editor_model = None + mock_args.agent_model = None + mock_args.editor_edit_format = None + mock_args.retries = None + mock_args.reasoning_effort = None + mock_args.thinking_tokens = None + mock_args.check_model_accepts_settings = True + mock_args.copy_paste = False + mock_args.hooks = None + + io = InputOutput(pretty=False, fancy_input=False, yes=True) + + # === SAVE SESSION === + coder_to_save = await Coder.create( + self.GPT35, "agent", io, args=mock_args, repo=mock.MagicMock() + ) + commands_to_save = Commands(io, coder_to_save, args=mock_args) + + # Configure state to be saved + await coder_to_save.mcp_manager.connect_server("mock_mcp") + coder_to_save.skills_manager.include_list = {"included_skill"} + coder_to_save.skills_manager.exclude_list = {"excluded_skill"} + coder_to_save.skills_manager.directory_paths = ["/test/skills/path/saved"] + + session_name = "agent_session" + await commands_to_save.execute("save-session", session_name) + + session_file = Path(handle_core_files(".cecli")) / "sessions" / f"{session_name}.json" + self.assertTrue(session_file.exists()) + + with open(session_file, "r", encoding="utf-8") as f: + saved_data = json.load(f) + + # Assert saved data is correct + self.assertEqual(saved_data["mcps"], ["mock_mcp"]) + self.assertEqual(saved_data["skills"]["skills_paths"], ["/test/skills/path/saved"]) + self.assertEqual(saved_data["skills"]["skills_includelist"], ["included_skill"]) + self.assertEqual(saved_data["skills"]["skills_excludelist"], ["excluded_skill"]) + self.assertEqual(saved_data["tools"]["tools_paths"], ["/test/tools/path"]) + self.assertEqual(saved_data["tools"]["tools_includelist"], ["included_tool"]) + self.assertEqual(saved_data["tools"]["tools_excludelist"], ["excluded_tool"]) + + # === LOAD SESSION === + # Create a new coder to load into, ensuring it's a clean slate + coder_to_load_initial = await Coder.create( + self.GPT35, "agent", io, args=mock_args, repo=mock.MagicMock() + ) + commands_to_load = Commands(io, coder_to_load_initial, args=mock_args) + + # Mock ToolRegistry.build_registry to check if it's called + with mock.patch( + "cecli.tools.utils.registry.ToolRegistry.build_registry" + ) as mock_build_registry: + coder_after_load = None + try: + await commands_to_load.execute("load-session", session_name) + except SwitchCoderSignal as e: + # The SwitchCoderSignal is expected, we need to get the new coder from it + coder_after_load = await Coder.create(**e.kwargs) + + self.assertIsNotNone(coder_after_load) + + # Assert loaded state is correct in the new coder instance + connected_mcps = {s.name for s in coder_after_load.mcp_manager.connected_servers} + self.assertIn("mock_mcp", connected_mcps) + + self.assertEqual( + coder_after_load.skills_manager.directory_paths, ["/test/skills/path/saved"] + ) + self.assertEqual(coder_after_load.skills_manager.include_list, {"included_skill"}) + self.assertEqual(coder_after_load.skills_manager.exclude_list, {"excluded_skill"}) + + self.assertEqual( + coder_after_load.agent_config["tools_paths"], ["/test/tools/path"] + ) + self.assertEqual( + coder_after_load.agent_config["tools_includelist"], ["included_tool"] + ) + self.assertEqual( + coder_after_load.agent_config["tools_excludelist"], ["excluded_tool"] + ) + + # Assert that the tool registry was rebuilt + mock_build_registry.assert_called_with(agent_config=coder_after_load.agent_config) From f4a5bffa7f536fd46445892b194a327957f3241a Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 27 Apr 2026 21:16:32 -0700 Subject: [PATCH 10/25] fix: Prevent strange characters on Windows TUI Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/tui/app.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cecli/tui/app.py b/cecli/tui/app.py index 464726a61c1..9a33d355fd5 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -5,6 +5,7 @@ import queue import time from functools import lru_cache +import platform from pathlib import Path import textual.strip @@ -359,6 +360,11 @@ def on_mouse_up(self, event: events.MouseUp) -> None: self._mouse_hold_timer = None self.update_key_hints(generating=self._currently_generating) + def on_mouse_move(self, event: events.MouseMove) -> None: + """Handle mouse move events to prevent strange characters on Windows.""" + if platform.system() == "Windows": + event.stop() + def _show_select_hint(self) -> None: """Show the shift+drag to select hint.""" try: From 390fb25f97dd0013e4c92b398ab18c45ee32d0cc Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 27 Apr 2026 22:10:33 -0700 Subject: [PATCH 11/25] fixed formatting --- cecli/coders/base_coder.py | 5 ++--- cecli/sessions.py | 37 ++++++++++++++++++------------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 0b7f847d436..0dd6203e298 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -1131,9 +1131,8 @@ def _include_in_map(abs_path): "other_files": other_files, "mentioned_fnames": mentioned_fnames, "all_abs_files": all_abs_files, - "read_only_count": ( - len(set(self.abs_read_only_fnames)) - + len(set(self.abs_read_only_stubs_fnames)) + "read_only_count": len(set(self.abs_read_only_fnames)) + len( + set(self.abs_read_only_stubs_fnames) ), } ) diff --git a/cecli/sessions.py b/cecli/sessions.py index 17489c9f623..a70bc098c8d 100644 --- a/cecli/sessions.py +++ b/cecli/sessions.py @@ -71,10 +71,9 @@ def list_sessions(self) -> List[Dict]: "file": session_file, "model": session_data.get("model", "unknown"), "edit_format": session_data.get("edit_format", "unknown"), - "num_messages": ( - len(session_data.get("chat_history", {}).get("done_messages", [])) - + len(session_data.get("chat_history", {}).get("cur_messages", [])) - ), + "num_messages": len( + session_data.get("chat_history", {}).get("done_messages", []) + ) + len(session_data.get("chat_history", {}).get("cur_messages", [])), "num_files": ( len(session_data.get("files", {}).get("editable", [])) + len(session_data.get("files", {}).get("read_only", [])) @@ -163,12 +162,16 @@ def _build_session_data(self, session_name) -> Dict: if hasattr(self.coder, "skills_manager") and self.coder.skills_manager: skills_data = { "skills_paths": [str(p) for p in self.coder.skills_manager.directory_paths], - "skills_includelist": list(self.coder.skills_manager.include_list) - if self.coder.skills_manager.include_list is not None - else [], - "skills_excludelist": list(self.coder.skills_manager.exclude_list) - if self.coder.skills_manager.exclude_list is not None - else [], + "skills_includelist": ( + list(self.coder.skills_manager.include_list) + if self.coder.skills_manager.include_list is not None + else [] + ), + "skills_excludelist": ( + list(self.coder.skills_manager.exclude_list) + if self.coder.skills_manager.exclude_list is not None + else [] + ), } agent_config_data = None @@ -189,11 +192,11 @@ def _build_session_data(self, session_name) -> Dict: "editor_edit_format": self.coder.main_model.editor_edit_format, "edit_format": self.coder.edit_format, "chat_history": { - "done_messages": ( - ConversationService.get_manager(self.coder).get_messages_dict(MessageTag.DONE) + "done_messages": ConversationService.get_manager(self.coder).get_messages_dict( + MessageTag.DONE ), - "cur_messages": ( - ConversationService.get_manager(self.coder).get_messages_dict(MessageTag.CUR) + "cur_messages": ConversationService.get_manager(self.coder).get_messages_dict( + MessageTag.CUR ), }, "files": { @@ -361,11 +364,7 @@ async def _apply_session_data(self, session_data: Dict, session_file: Path) -> b # Load skills skills_data = session_data.get("skills") - if ( - skills_data - and hasattr(self.coder, "skills_manager") - and self.coder.skills_manager - ): + if skills_data and hasattr(self.coder, "skills_manager") and self.coder.skills_manager: self.coder.skills_manager.directory_paths = skills_data.get("skills_paths", []) self.coder.skills_manager.include_list = set( skills_data.get("skills_includelist", []) From fe1b37de636b72cd300de282a29e3545382001d2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 28 Apr 2026 12:57:12 -0700 Subject: [PATCH 12/25] cli-15: fixed formatting --- cecli/tools/edit_text.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cecli/tools/edit_text.py b/cecli/tools/edit_text.py index f03fc6d96df..5b4d64f7c3c 100644 --- a/cecli/tools/edit_text.py +++ b/cecli/tools/edit_text.py @@ -56,9 +56,9 @@ class Tool(BaseTool): "type": "string", "enum": ["replace", "delete", "insert"], "description": ( - "The type of operation: 'replace' (replace range with text), " - "'delete' (remove range), or 'insert' (insert text after start_line). " - "Defaults to 'replace'." + "The type of operation: 'replace' (replace range with" + " text), 'delete' (remove range), or 'insert' (insert text" + " after start_line). Defaults to 'replace'." ), }, "text": { @@ -78,8 +78,8 @@ class Tool(BaseTool): "end_line": { "type": "string", "description": ( - 'Hashline format for end line: "{4 char hash}" (without the ' - "braces)" + 'Hashline format for end line: "{4 char hash}" (without the' + " braces)" ), }, }, @@ -179,8 +179,8 @@ def execute( if operation in ("replace", "delete"): if edit_start_line is None: raise ToolError( - f"Edit {edit_index + 1}: 'start_line' parameter is required " - f"for '{operation}' operation" + f"Edit {edit_index + 1}: 'start_line' parameter is required" + f" for '{operation}' operation" ) if edit_end_line is None: raise ToolError( @@ -190,8 +190,8 @@ def execute( if operation == "insert": if edit_start_line is None: raise ToolError( - f"Edit {edit_index + 1}: 'start_line' parameter is required " - "for 'insert' operation" + f"Edit {edit_index + 1}: 'start_line' parameter is required" + " for 'insert' operation" ) # For insert, end_line defaults to start_line edit_end_line = edit_end_line or edit_start_line From d390bb365a31706698201c3047a8adf20275fc8a Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 28 Apr 2026 19:31:28 -0700 Subject: [PATCH 13/25] test: Add tests for app module --- tests/tui/test_app.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/tui/test_app.py diff --git a/tests/tui/test_app.py b/tests/tui/test_app.py new file mode 100644 index 00000000000..e69de29bb2d From 3cc115efaafa963c3e2310064d15c8917d0925d8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 28 Apr 2026 19:31:31 -0700 Subject: [PATCH 14/25] test: add TUI mouse event handling tests Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- tests/tui/test_app.py | 50 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/tui/test_app.py b/tests/tui/test_app.py index e69de29bb2d..28a16d6f4cc 100644 --- a/tests/tui/test_app.py +++ b/tests/tui/test_app.py @@ -0,0 +1,50 @@ +import unittest +from unittest.mock import MagicMock, patch + +from textual import events + +# Assuming TUI is in cecli.tui.app +from cecli.tui.app import TUI + + +class TestTUI(unittest.TestCase): + @patch("cecli.tui.app.TUI.__init__", return_value=None) + def setUp(self, mock_init): + self.tui = TUI(coder_worker=None, output_queue=None, input_queue=None, args=None) + # Mock attributes that might be accessed in on_mouse_move or its calls + self.tui._mouse_hold_timer = None + self.tui._currently_generating = False + + def test_on_mouse_move_windows(self): + """ + Test that on_mouse_move stops the event on Windows. + """ + # Mock the platform system to return "Windows" + with patch("platform.system", return_value="Windows"): + # Create a mock mouse move event + mock_event = MagicMock(spec=events.MouseMove) + + # Call the event handler + self.tui.on_mouse_move(mock_event) + + # Assert that event.stop() was called + mock_event.stop.assert_called_once() + + def test_on_mouse_move_linux(self): + """ + Test that on_mouse_move does not stop the event on Linux. + """ + # Mock the platform system to return "Linux" + with patch("platform.system", return_value="Linux"): + # Create a mock mouse move event + mock_event = MagicMock(spec=events.MouseMove) + + # Call the event handler + self.tui.on_mouse_move(mock_event) + + # Assert that event.stop() was not called + mock_event.stop.assert_not_called() + + +if __name__ == "__main__": + unittest.main() From 198737fa394607d999191197f254e0a20b16ed07 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 28 Apr 2026 19:33:04 -0700 Subject: [PATCH 15/25] test: refactor tests/tui/test_app.py to use pytest Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- tests/tui/test_app.py | 71 ++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/tests/tui/test_app.py b/tests/tui/test_app.py index 28a16d6f4cc..fe0facdcf69 100644 --- a/tests/tui/test_app.py +++ b/tests/tui/test_app.py @@ -1,4 +1,4 @@ -import unittest +import pytest from unittest.mock import MagicMock, patch from textual import events @@ -7,44 +7,31 @@ from cecli.tui.app import TUI -class TestTUI(unittest.TestCase): - @patch("cecli.tui.app.TUI.__init__", return_value=None) - def setUp(self, mock_init): - self.tui = TUI(coder_worker=None, output_queue=None, input_queue=None, args=None) - # Mock attributes that might be accessed in on_mouse_move or its calls - self.tui._mouse_hold_timer = None - self.tui._currently_generating = False - - def test_on_mouse_move_windows(self): - """ - Test that on_mouse_move stops the event on Windows. - """ - # Mock the platform system to return "Windows" - with patch("platform.system", return_value="Windows"): - # Create a mock mouse move event - mock_event = MagicMock(spec=events.MouseMove) - - # Call the event handler - self.tui.on_mouse_move(mock_event) - - # Assert that event.stop() was called - mock_event.stop.assert_called_once() - - def test_on_mouse_move_linux(self): - """ - Test that on_mouse_move does not stop the event on Linux. - """ - # Mock the platform system to return "Linux" - with patch("platform.system", return_value="Linux"): - # Create a mock mouse move event - mock_event = MagicMock(spec=events.MouseMove) - - # Call the event handler - self.tui.on_mouse_move(mock_event) - - # Assert that event.stop() was not called - mock_event.stop.assert_not_called() - - -if __name__ == "__main__": - unittest.main() +@pytest.fixture +def tui_instance(monkeypatch): + """A pytest fixture to create a mocked TUI instance.""" + monkeypatch.setattr("cecli.tui.app.TUI.__init__", lambda *args, **kwargs: None) + tui = TUI(coder_worker=None, output_queue=None, input_queue=None, args=None) + tui._mouse_hold_timer = None + tui._currently_generating = False + return tui + + +def test_on_mouse_move_windows(tui_instance): + """ + Test that on_mouse_move stops the event on Windows. + """ + with patch("platform.system", return_value="Windows"): + mock_event = MagicMock(spec=events.MouseMove) + tui_instance.on_mouse_move(mock_event) + mock_event.stop.assert_called_once() + + +def test_on_mouse_move_linux(tui_instance): + """ + Test that on_mouse_move does not stop the event on Linux. + """ + with patch("platform.system", return_value="Linux"): + mock_event = MagicMock(spec=events.MouseMove) + tui_instance.on_mouse_move(mock_event) + mock_event.stop.assert_not_called() From 5a613b6473d7364b208facd3dbe5efcce8c164b8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 28 Apr 2026 20:13:06 -0700 Subject: [PATCH 16/25] cli-15: fixed formatting --- cecli/coders/base_coder.py | 5 +++-- cecli/sessions.py | 15 ++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 0dd6203e298..0b7f847d436 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -1131,8 +1131,9 @@ def _include_in_map(abs_path): "other_files": other_files, "mentioned_fnames": mentioned_fnames, "all_abs_files": all_abs_files, - "read_only_count": len(set(self.abs_read_only_fnames)) + len( - set(self.abs_read_only_stubs_fnames) + "read_only_count": ( + len(set(self.abs_read_only_fnames)) + + len(set(self.abs_read_only_stubs_fnames)) ), } ) diff --git a/cecli/sessions.py b/cecli/sessions.py index a70bc098c8d..c1e9fbdc5f3 100644 --- a/cecli/sessions.py +++ b/cecli/sessions.py @@ -71,9 +71,10 @@ def list_sessions(self) -> List[Dict]: "file": session_file, "model": session_data.get("model", "unknown"), "edit_format": session_data.get("edit_format", "unknown"), - "num_messages": len( - session_data.get("chat_history", {}).get("done_messages", []) - ) + len(session_data.get("chat_history", {}).get("cur_messages", [])), + "num_messages": ( + len(session_data.get("chat_history", {}).get("done_messages", [])) + + len(session_data.get("chat_history", {}).get("cur_messages", [])) + ), "num_files": ( len(session_data.get("files", {}).get("editable", [])) + len(session_data.get("files", {}).get("read_only", [])) @@ -192,11 +193,11 @@ def _build_session_data(self, session_name) -> Dict: "editor_edit_format": self.coder.main_model.editor_edit_format, "edit_format": self.coder.edit_format, "chat_history": { - "done_messages": ConversationService.get_manager(self.coder).get_messages_dict( - MessageTag.DONE + "done_messages": ( + ConversationService.get_manager(self.coder).get_messages_dict(MessageTag.DONE) ), - "cur_messages": ConversationService.get_manager(self.coder).get_messages_dict( - MessageTag.CUR + "cur_messages": ( + ConversationService.get_manager(self.coder).get_messages_dict(MessageTag.CUR) ), }, "files": { From eae434efc94ea75dcb84fdc4c753a61a4c9a1129 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 28 Apr 2026 20:13:52 -0700 Subject: [PATCH 17/25] cli-14: fixed formatting --- cecli/tui/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cecli/tui/app.py b/cecli/tui/app.py index 9a33d355fd5..fc87bd7211b 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -2,10 +2,10 @@ import concurrent.futures import json +import platform import queue import time from functools import lru_cache -import platform from pathlib import Path import textual.strip From 2566f6c4cee68da3479e047c37a3b579aae2ab10 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 28 Apr 2026 20:21:20 -0700 Subject: [PATCH 18/25] cli-15: fixed formatting --- tests/basic/test_sessions.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/basic/test_sessions.py b/tests/basic/test_sessions.py index aafc8105756..aa26f4f5a26 100644 --- a/tests/basic/test_sessions.py +++ b/tests/basic/test_sessions.py @@ -297,9 +297,7 @@ async def test_cmd_save_load_session_agent_config(self): self.assertEqual(coder_after_load.skills_manager.include_list, {"included_skill"}) self.assertEqual(coder_after_load.skills_manager.exclude_list, {"excluded_skill"}) - self.assertEqual( - coder_after_load.agent_config["tools_paths"], ["/test/tools/path"] - ) + self.assertEqual(coder_after_load.agent_config["tools_paths"], ["/test/tools/path"]) self.assertEqual( coder_after_load.agent_config["tools_includelist"], ["included_tool"] ) From 63473d75ef49a970d106e9211896a80fdc9efb04 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 28 Apr 2026 20:21:54 -0700 Subject: [PATCH 19/25] cli-14: fixed formatting --- tests/tui/test_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tui/test_app.py b/tests/tui/test_app.py index fe0facdcf69..e6244d87cf2 100644 --- a/tests/tui/test_app.py +++ b/tests/tui/test_app.py @@ -1,6 +1,6 @@ -import pytest from unittest.mock import MagicMock, patch +import pytest from textual import events # Assuming TUI is in cecli.tui.app From 05e431e6db98b2f45b889a7f12fabbcdc8d1708b Mon Sep 17 00:00:00 2001 From: BecoKo Date: Wed, 29 Apr 2026 18:28:05 +0300 Subject: [PATCH 20/25] Error in UpdateTodoList: [Errno 2] No such file or directory fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Win11 I started cecli with option '--subtree-only' in a subfolder of my repo. Agent failed to update todo list with error message: Unable to write file C:\SandBox\repo1\.cecli\agents\2026-04-29\cab13e36-34e7-433a-8431-4147676a23ca\todo.txt: [Errno 2] No such file or ▃ directory: 'C:\\SandBox\\repo1\\.cecli\\agents\\2026-04-29\\cab13e36-34e7-433a-8431-4147676a23ca\\todo.txt' Error in UpdateTodoList: [Errno 2] No such file or directory: 'C:\\SandBox\\repo1\\.cecli\\agents\\2026-04-29\\cab13e36-34e7-433a-8431-4147676a23ca\\todo.txt' Traceback (most recent call last): File "C:\Python\cecli-dev-venv\Lib\site-packages\cecli\tools\update_todo_list.py", line 158, in execute coder.io.write_text(abs_path, new_content) File "C:\Python\cecli-dev-venv\Lib\site-packages\cecli\io.py", line 737, in write_text with open(str(filename), "w", encoding=self.encoding, newline=newline) as f: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FileNotFoundError: [Errno 2] No such file or directory: 'C:\\SandBox\\repo1\\.cecli\\agents\\2026-04-29\\cab13e36-34e7-433a-8431-4147676a23ca\\todo.txt' This commit fix the error for me. --- cecli/coders/base_coder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 0b7f847d436..8d16f741998 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -4003,7 +4003,8 @@ def apply_edits_dry_run(self, edits): return edits def local_agent_folder(self, path): - os.makedirs(f".cecli/agents/{GLOBAL_DATE}/{self.uuid}", exist_ok=True) + abs_path = self.abs_root_path(f".cecli/agents/{GLOBAL_DATE}/{self.uuid}/path") + os.makedirs(abs_path, exist_ok=True) stripped = path.lstrip("/") return f".cecli/agents/{GLOBAL_DATE}/{self.uuid}/{stripped}" From 70c2ed00c2d8bfa4c02f1219784e568ddb2da9ef Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Apr 2026 18:45:18 -0400 Subject: [PATCH 21/25] Bump Version --- cecli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cecli/__init__.py b/cecli/__init__.py index 61710003ccb..85f102b2b96 100644 --- a/cecli/__init__.py +++ b/cecli/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.99.7.dev" +__version__ = "0.99.9.dev" safe_version = __version__ try: From 91967daf87e2434b06ffab8458fa61f7da5454cc Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Apr 2026 18:45:58 -0400 Subject: [PATCH 22/25] Change repo map assistant message --- cecli/helpers/conversation/integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cecli/helpers/conversation/integration.py b/cecli/helpers/conversation/integration.py index aba4f584670..85776ac53db 100644 --- a/cecli/helpers/conversation/integration.py +++ b/cecli/helpers/conversation/integration.py @@ -446,7 +446,7 @@ def add_repo_map_messages(self) -> List[Dict[str, Any]]: dict(role="user", content=repo_content), dict( role="assistant", - content="Ok, I won't try and edit those files without asking first.", + content="Thank you, these files will help with navigating the codebase.", ), ] From cc91c9c8e9374d4a7d5f85e7a92c8b297a1bd5b9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Apr 2026 19:37:44 -0400 Subject: [PATCH 23/25] Small formatting changes, remove unnecessary unicode chars near end of context, message about not editing files so the LLM doesn't think it's okay to not do work --- cecli/coders/agent_coder.py | 8 ++------ cecli/prompts/agent.yml | 6 +++++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cecli/coders/agent_coder.py b/cecli/coders/agent_coder.py index 30f52a193fb..d261e772222 100644 --- a/cecli/coders/agent_coder.py +++ b/cecli/coders/agent_coder.py @@ -618,9 +618,7 @@ def get_context_summary(self): total_file_tokens += tokens editable_tokens += tokens size_indicator = ( - "🔴 Large" - if tokens > 5000 - else "🟡 Medium" if tokens > 1000 else "🟢 Small" + "Large" if tokens > 5000 else "Medium" if tokens > 1000 else "Small" ) editable_files.append( f"- {rel_fname}: {tokens:,} tokens ({size_indicator})" @@ -642,9 +640,7 @@ def get_context_summary(self): total_file_tokens += tokens readonly_tokens += tokens size_indicator = ( - "🔴 Large" - if tokens > 5000 - else "🟡 Medium" if tokens > 1000 else "🟢 Small" + "Large" if tokens > 5000 else "Medium" if tokens > 1000 else "Small" ) readonly_files.append( f"- {rel_fname}: {tokens:,} tokens ({size_indicator})" diff --git a/cecli/prompts/agent.yml b/cecli/prompts/agent.yml index dabba8ec53b..2c716a8889b 100644 --- a/cecli/prompts/agent.yml +++ b/cecli/prompts/agent.yml @@ -14,6 +14,10 @@ files_no_full_files_with_repo_map: | I have a repository map. I will use it to target my navigation and add relevant files to the context. +repo_content_prefix: | + Here are summaries of some files present in my git repository. + These files should be helpful for navigating the codebase. + main_system: | ## Core Directives @@ -34,7 +38,7 @@ main_system: | uXdn::def example_method(): WAR5:: return "example" vwkS:: - + ## Core Workflow From 67c221a9ebd66ac86b87a3aed6a0793527ace6c8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Apr 2026 20:12:11 -0400 Subject: [PATCH 24/25] Fix GetLines setting output content before the tool call in message stream by queueing and flushing the FILE_CONTEXT messages --- cecli/coders/base_coder.py | 2 ++ cecli/helpers/conversation/integration.py | 4 ++-- cecli/helpers/conversation/manager.py | 28 +++++++++++++++++++++++ cecli/tools/get_lines.py | 14 +++++++----- cecli/tools/utils/helpers.py | 10 ++++---- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 0b7f847d436..74d60bdc508 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -2178,6 +2178,8 @@ async def send_message(self, inp): # Notify IO that LLM processing is starting self.io.llm_started() + ConversationService.get_manager(self).flush_queue() + if inp: # Make sure current coder actually has control of conversation system ConversationService.get_chunks(self).initialize_conversation_system() diff --git a/cecli/helpers/conversation/integration.py b/cecli/helpers/conversation/integration.py index 85776ac53db..00ee834e004 100644 --- a/cecli/helpers/conversation/integration.py +++ b/cecli/helpers/conversation/integration.py @@ -781,14 +781,14 @@ def add_file_context_messages(self, promote_messages=True) -> None: } # Add to conversation manager - ConversationService.get_manager(coder).add_message( + ConversationService.get_manager(coder).queue_message( message_dict=user_msg, tag=MessageTag.FILE_CONTEXTS, hash_key=("file_context_user", file_path), force=True, ) - ConversationService.get_manager(coder).add_message( + ConversationService.get_manager(coder).queue_message( message_dict=assistant_msg, tag=MessageTag.FILE_CONTEXTS, hash_key=("file_context_assistant", file_path), diff --git a/cecli/helpers/conversation/manager.py b/cecli/helpers/conversation/manager.py index 5561979189b..93c66e8164d 100644 --- a/cecli/helpers/conversation/manager.py +++ b/cecli/helpers/conversation/manager.py @@ -25,6 +25,7 @@ def __init__(self, coder): self._tag_cache: Dict[str, List[Dict[str, Any]]] = {} self._ALL_MESSAGES_CACHE_KEY = "__all__" self.DEFAULT_TAG_PROMOTION_VALUE: int = 999 + self._queue: List[Dict[str, Any]] = [] @classmethod def get_instance(cls, coder) -> "ConversationManager": @@ -193,6 +194,33 @@ def add_message( self._tag_cache.pop(self._ALL_MESSAGES_CACHE_KEY, None) return message + def queue_message(self, **kwargs) -> None: + """ + Queue an add_message() call for later insertion. + + Accepts the same keyword arguments as add_message() + and stores them in an internal queue to be flushed + later via flush_queue(). + """ + self._queue.append(kwargs) + + def flush_queue(self) -> List[Any]: + """ + Flush all queued add_message() calls. + + Calls add_message() for each set of kwargs in the + internal queue, then clears the queue. + + Returns: + List of BaseMessage instances returned by add_message() + """ + results = [] + while self._queue: + kwargs = self._queue.pop(0) + result = self.add_message(**kwargs) + results.append(result) + return results + def base_sort(self, messages: List[BaseMessage]) -> List[BaseMessage]: """ Sorts messages by effective priority (promotion if mark_for_demotion has not elapsed yet), then timestamp. diff --git a/cecli/tools/get_lines.py b/cecli/tools/get_lines.py index a7f39982032..138428b4da0 100644 --- a/cecli/tools/get_lines.py +++ b/cecli/tools/get_lines.py @@ -21,12 +21,14 @@ class Tool(BaseTool): "description": ( "Get hashline prefixes of context between start and end patterns in multiple files." " Accepts an array of show objects, each with file_path, start_text," - " end_text, and optional padding. Special markers '@000' and '000@' can be" - " used for start_text and end_text to represent the first and last lines of" - " the file respectively. Never use hashlines as the start_text and end_text" - " values. These values must be lines from the content of the file." + " end_text, and optional padding." + " These values must be lines from the content of the file." " They can contain up to 3 lines but newlines should generally be avoided." - " Avoid using generic keywords." + " Avoid using generic keywords and symbols. Special markers '@000' and '000@' can be" + " used for start_text and end_text to represent the first and last lines of" + " the file respectively. Avoid using the special markers on files with contents." + " They are intended to be used with empty files." + " Never use hashlines as the start_text and end_text values." " Do not use the same pattern for the start_text and end_text." " It is usually best to use function names and other block identifiers as " " start_texts and end_texts." @@ -279,7 +281,7 @@ def execute(cls, coder, show, **kwargs): coder.io.tool_output("File contents already up to date") return ( "File contents already up to date." - "Do not call GetLines again with these parameters until you edit the file." + " Do not call GetLines again with these parameters until you edit the file." ) else: coder.io.tool_output(f"✅ Successfully retrieved context for {len(show)} file(s)") diff --git a/cecli/tools/utils/helpers.py b/cecli/tools/utils/helpers.py index 45e123d91f5..d3c219383bb 100644 --- a/cecli/tools/utils/helpers.py +++ b/cecli/tools/utils/helpers.py @@ -55,11 +55,11 @@ def validate_file_for_edit(coder, file_path): raise ToolError( f"File '{file_path}' is read-only. Make editable with `ContextManager` first." ) - else: - # File exists but is not in context at all - raise ToolError( - f"File '{file_path}' not in context. Make editable with `ContextManager` first." - ) + # else: + # # File exists but is not in context at all + # raise ToolError( + # f"File '{file_path}' not in context. Make editable with `ContextManager` first." + # ) # Reread content immediately before potential modification content = coder.io.read_text(abs_path) From cac2e5f5a968772045f37cc5a377dc5cc426567f Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Apr 2026 21:02:49 -0400 Subject: [PATCH 25/25] Preserve agent folder file structure --- cecli/coders/base_coder.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 6d065651721..8c366dcbc2a 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -4005,8 +4005,10 @@ def apply_edits_dry_run(self, edits): return edits def local_agent_folder(self, path): - abs_path = self.abs_root_path(f".cecli/agents/{GLOBAL_DATE}/{self.uuid}/path") - os.makedirs(abs_path, exist_ok=True) + os.makedirs( + self.abs_root_path(f".cecli/agents/{GLOBAL_DATE}/{self.uuid}"), + exist_ok=True, + ) stripped = path.lstrip("/") return f".cecli/agents/{GLOBAL_DATE}/{self.uuid}/{stripped}"