From 0e2cafccb01fd8b00809fb1bb53363105cf3c942 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Apr 2026 19:24:56 -0700 Subject: [PATCH 01/16] fix: Escape special characters in tool descriptions Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/models.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cecli/models.py b/cecli/models.py index 0a48ae1304b..1fc91d3e29d 100644 --- a/cecli/models.py +++ b/cecli/models.py @@ -1172,6 +1172,26 @@ async def send_completion( sorted_tools = sorted( effective_tools, key=lambda x: x.get("function", {}).get("name", "Invalid Name") ) + + try: + # Deep copy to avoid modifying original tool schemas + sorted_tools = json.loads(json.dumps(sorted_tools)) + + for tool in sorted_tools: + function_schema = tool.get("function") + if function_schema and "description" in function_schema: + desc = function_schema.get("description") + if isinstance(desc, str): + # Escape the description string for JSON, but without the outer quotes. + # This is a workaround for issues with special characters in descriptions. + function_schema["description"] = json.dumps(desc, ensure_ascii=False)[ + 1:-1 + ] + except (TypeError, json.JSONDecodeError): + # If deep copy fails, proceed with original tools. + # This is a safeguard. + pass + kwargs["tools"] = sorted_tools if functions and len(functions) == 1: From 8c11bb0676b2964fd56e948acaa65596dc839850 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Apr 2026 19:38:56 -0700 Subject: [PATCH 02/16] fix: Add test for tool description special character escaping Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- tests/basic/test_models.py | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/basic/test_models.py b/tests/basic/test_models.py index 82a765171ad..9138d49f37f 100644 --- a/tests/basic/test_models.py +++ b/tests/basic/test_models.py @@ -810,3 +810,51 @@ def test_print_matching_models_price_formatting(self): output_found = any("$10.50/1m/output" in call for call in calls) assert input_found, "Input pricing format incorrect" assert output_found, "Output pricing format incorrect" + + @patch("cecli.models.litellm.acompletion") + async def test_tool_description_escaping(self, mock_acompletion): + """ + Test that tool descriptions with special characters are properly escaped. + """ + model = Model("gpt-4") + messages = [{"role": "user", "content": "Hello"}] + + # A complex description with various special characters + complex_description = 'This is a "test" description with `special` characters like \\, \n, and *.' + + # Mock tool with the complex description + mock_tool = { + "type": "function", + "function": { + "name": "test_tool", + "description": complex_description, + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, + } + + await model.send_completion(messages, functions=None, stream=False, tools=[mock_tool]) + + # Verify that acompletion was called + mock_acompletion.assert_called_once() + + # Get the keyword arguments passed to acompletion + call_kwargs = mock_acompletion.call_args.kwargs + + # Check that the 'tools' argument is present and correctly formatted + assert "tools" in call_kwargs + sent_tools = call_kwargs["tools"] + assert isinstance(sent_tools, list) + assert len(sent_tools) == 1 + + # Verify the description of the sent tool + sent_tool_function = sent_tools[0].get("function", {}) + sent_description = sent_tool_function.get("description") + + # The description should be a JSON-escaped string + # Expected: 'This is a \\"test\\" description with `special` characters like \\\\, \\n, and *.' + expected_escaped_description = 'This is a \\"test\\" description with `special` characters like \\\\, \\n, and *.' + assert sent_description == expected_escaped_description From 89f2499c2191f1574075f977611736608325e2fa Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 8 May 2026 19:42:20 -0700 Subject: [PATCH 03/16] fix: Hide PowerShell window when sending notifications Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cecli/io.py b/cecli/io.py index 230b87f0745..c3ebac99f6b 100644 --- a/cecli/io.py +++ b/cecli/io.py @@ -1724,7 +1724,7 @@ def get_default_notification_command(self): "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('cecli')" '.Show($toast)"' ) - return "powershell -command" + ps_command + return "powershell -WindowStyle Hidden -Command" + ps_command return None # Unknown system From 3d0f6b602e16f826b2bf929d271a1cd035b80d7f Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 10 May 2026 10:00:56 -0400 Subject: [PATCH 04/16] Bump Version --- cecli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cecli/__init__.py b/cecli/__init__.py index 49025eeddbe..32ad82fb67b 100644 --- a/cecli/__init__.py +++ b/cecli/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.99.10.dev" +__version__ = "0.99.12.dev" safe_version = __version__ try: From 2104bfbbef48a5d181f40613bb383fe00b19d823 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 10 May 2026 12:38:17 -0400 Subject: [PATCH 05/16] Throttle observations just in case large tool responses would retrigger one --- cecli/coders/base_coder.py | 7 +++---- cecli/helpers/observations/manager.py | 13 ++++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 922d5328671..8ef7336cf6e 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -453,7 +453,6 @@ def __init__( # Initialize conversation system if enabled ConversationService.get_chunks(self).initialize_conversation_system() - self.observation_manager = ObservationManager.get_instance(self) self.commands = commands or Commands(self.io, self, args=args) self.commands.coder = self @@ -1752,7 +1751,7 @@ async def compact_context_if_needed(self, force=False, message=""): return # Trigger background observation/reflection check - await self.observation_manager.check_and_trigger() + await ObservationManager.get_instance(self).check_and_trigger() manager = ConversationService.get_manager(self) done_messages = manager.get_messages_dict(MessageTag.DONE) @@ -1789,8 +1788,8 @@ async def summarize_and_update(messages, tag): if not text: raise ValueError(f"Summarization of {tag} messages returned empty.") - if self.observation_manager.observations: - obs_text = "\n".join(self.observation_manager.observations) + if ObservationManager.get_instance(self).observations: + obs_text = "\n".join(ObservationManager.get_instance(self).observations) text = f"HISTORICAL OBSERVATIONS:\n{obs_text}\n\n{text}" manager.clear_tag(tag) diff --git a/cecli/helpers/observations/manager.py b/cecli/helpers/observations/manager.py index 3d0ebf5d864..81a44f326c9 100644 --- a/cecli/helpers/observations/manager.py +++ b/cecli/helpers/observations/manager.py @@ -26,17 +26,21 @@ async def check_and_trigger(self): if self.is_processing: return - manager = ConversationService.get_manager(self.coder) - cur_messages = manager.get_messages_dict(MessageTag.CUR) + cur_messages = ConversationService.get_manager(self.coder).get_messages_dict(MessageTag.CUR) # Calculate unobserved tokens unobserved = cur_messages[self._last_observed_index :] + current_index = len(cur_messages) + if not unobserved: return tokens = self.coder.summarizer.count_tokens(unobserved) - if tokens >= self.observation_threshold: + if ( + tokens >= self.observation_threshold + and (not self._last_observed_index or current_index - self._last_observed_index >= 10) + ) or tokens >= 2 * self.observation_threshold: asyncio.create_task(self.run_observation(unobserved)) self._last_observed_index = len(cur_messages) @@ -50,8 +54,7 @@ async def check_and_trigger(self): async def run_observation(self, messages): self.is_processing = True try: - manager = ConversationService.get_manager(self.coder) - all_messages = manager.get_messages_dict() + all_messages = ConversationService.get_manager(self.coder).get_messages_dict() prompt = self.coder.gpt_prompts.observation_prompt observation = await self.coder.summarizer.summarize_all_as_text( all_messages, prompt, max_tokens=8192 From 2e89564add20e4a2313907d5542f1a86406793a3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 10 May 2026 14:03:16 -0700 Subject: [PATCH 06/16] fix: Use MessageBox for Windows notifications to hide terminal Co-authored-by: cecli (openai/nvidia_nim/deepseek-ai/deepseek-v4-pro) --- cecli/io.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/cecli/io.py b/cecli/io.py index c3ebac99f6b..455e37acfe4 100644 --- a/cecli/io.py +++ b/cecli/io.py @@ -1707,24 +1707,13 @@ def get_default_notification_command(self): return f"zenity --notification --text='{NOTIFICATION_MESSAGE}'" return None # No known notification tool found elif system == "Windows": - # PowerShell toast notification - ps_command = ( - ' "try {{ Add-Type -AssemblyName System.Runtime.WindowsRuntime; $null =' - " [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications," - " ContentType = WindowsRuntime] }} catch {{}}; " - "$template =" - " [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent" - "([Windows.UI.Notifications.ToastTemplateType]::ToastText02); " - "$toastXml = $template.GetXml(); " - "$toastXml.GetElementsByTagName('text')[0].AppendChild" - "($template.CreateTextNode('cecli')) > $null; " - f"$toastXml.GetElementsByTagName('text')[1].AppendChild" - f"($template.CreateTextNode('{NOTIFICATION_MESSAGE}')) > $null; " - "$toast = [Windows.UI.Notifications.ToastNotification]::new($toastXml); " - "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('cecli')" - '.Show($toast)"' + # PowerShell MessageBox notification (no terminal window shown) + return ( + "powershell -Command" + " \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms');" + f" [System.Windows.Forms.MessageBox]::Show('{NOTIFICATION_MESSAGE}'," + " 'cecli')\"" ) - return "powershell -WindowStyle Hidden -Command" + ps_command return None # Unknown system From c9f8771cc04871d636f26776b33d68f95f7ef6c9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 10 May 2026 14:20:39 -0700 Subject: [PATCH 07/16] fix: Remove DETACHED_PROCESS flag for Windows notifications Co-authored-by: cecli (openai/nvidia_nim/deepseek-ai/deepseek-v4-pro) --- cecli/io.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cecli/io.py b/cecli/io.py index 455e37acfe4..cd7994a64e0 100644 --- a/cecli/io.py +++ b/cecli/io.py @@ -1728,9 +1728,7 @@ def _send_notification(self): "stderr": subprocess.DEVNULL, } if platform.system() == "Windows": - kwargs["creationflags"] = ( - subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS - ) + kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW else: # For non-Windows systems, start a new session to detach kwargs["start_new_session"] = True From 40a8f2390c0bfffa48424deee4299f65dbb33b72 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 10 May 2026 14:24:07 -0700 Subject: [PATCH 08/16] cli-12: fixed formatting --- cecli/coders/agent_coder.py | 25 +++++++++++++-------- cecli/coders/base_coder.py | 5 ++--- cecli/sessions.py | 15 ++++++------- cecli/tools/context_manager.py | 3 +-- cecli/tools/read_range.py | 41 +++++++++++++++++++++------------- 5 files changed, 51 insertions(+), 38 deletions(-) diff --git a/cecli/coders/agent_coder.py b/cecli/coders/agent_coder.py index 77db195c201..8524d707185 100644 --- a/cecli/coders/agent_coder.py +++ b/cecli/coders/agent_coder.py @@ -282,9 +282,7 @@ async def _exec_async(): content_parts.append(item.text) return "".join(content_parts) except Exception as e: - self.io.tool_warning( - (f"Executing {tool_name} on {server.name} failed:\nError: {e}") - ) + self.io.tool_warning(f"Executing {tool_name} on {server.name} failed:\nError: {e}") return f"Error executing tool call {tool_name}: {e}" result, interrupted = await interruptible(_exec_async(), self.interrupt_event) @@ -381,7 +379,10 @@ def get_context_symbol_outline(self): try: result = '\n' result += "## Symbol Outline (Current Context)\n\n" - result += "Code definitions (classes, functions, methods, etc.) found in files currently in chat context." + result += ( + "Code definitions (classes, functions, methods, etc.) found in files currently in" + " chat context." + ) result += "\n\n" files_to_outline = list(self.abs_fnames) + list(self.abs_read_only_fnames) if not files_to_outline: @@ -522,7 +523,10 @@ def get_context_summary(self): ) if editable_files: result += "\n".join(editable_files) + "\n\n" - result += f"**Total editable: {len(editable_files)} files, {editable_tokens:,} tokens**\n\n" + result += ( + f"**Total editable: {len(editable_files)} files," + f" {editable_tokens:,} tokens**\n\n" + ) else: result += "No editable files in context\n\n" if self.abs_read_only_fnames: @@ -542,7 +546,10 @@ def get_context_summary(self): ) if readonly_files: result += "\n".join(readonly_files) + "\n\n" - result += f"**Total read-only: {len(readonly_files)} files, {readonly_tokens:,} tokens**\n\n" + result += ( + f"**Total read-only: {len(readonly_files)} files," + f" {readonly_tokens:,} tokens**\n\n" + ) else: result += "No read-only files in context\n\n" extra_tokens = sum(self.context_block_tokens.values()) @@ -730,7 +737,7 @@ async def gather_and_await(): self.model_kwargs = {} result_message = f"Error executing {tool_name}: {e}" self.io.tool_error( - (f"Error during {tool_name} execution: {e}\n{traceback.format_exc()}") + f"Error during {tool_name} execution: {e}\n{traceback.format_exc()}" ) tool_responses.append( {"role": "tool", "tool_call_id": tool_call.id, "content": result_message} @@ -1020,8 +1027,8 @@ def _generate_tool_context(self, repetitive_tools): context_parts.append("## File Editing Tools Disabled") context_parts.append( "File editing tools are currently disabled. Use `ReadRange` to determine the" - " current content hash prefixes needed to perform an edit and activate them when you" - " are ready to edit a file." + " current content hash prefixes needed to perform an edit and activate them when" + " you are ready to edit a file." ) context_parts.append("\n\n") diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 922d5328671..afcae12a9d7 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -1142,9 +1142,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 5ff6a5cd4e9..6d6be87bc8c 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", [])) @@ -198,11 +197,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": { diff --git a/cecli/tools/context_manager.py b/cecli/tools/context_manager.py index f969f48edcc..de565573f15 100644 --- a/cecli/tools/context_manager.py +++ b/cecli/tools/context_manager.py @@ -32,8 +32,7 @@ class Tool(BaseTool): "type": "array", "items": {"type": "string"}, "description": ( - "List of file paths to add as read-only. " - "Limit to at most 2 at a time." + "List of file paths to add as read-only. Limit to at most 2 at a time." ), }, "create": { diff --git a/cecli/tools/read_range.py b/cecli/tools/read_range.py index 0b0310ab997..66cafedd73b 100644 --- a/cecli/tools/read_range.py +++ b/cecli/tools/read_range.py @@ -22,17 +22,16 @@ class Tool(BaseTool): "name": "ReadRange", "description": ( "Get content hash prefixes of content between start and end patterns in files." - " Accepts an array of `show` objects, each with file_path, start_text," - " end_text. 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 and symbols." - "Special markers @000 and 000@ represent the file boundaries and can be" - " used for start_text and end_text for the first and last lines of" - " the file respectively. Avoid using both of the special markers together on non-empty files." - " Never use content hashes as the start_text and end_text values." - " Do not use the same pattern for the start_text and end_text." - " It is best to use function names, variable declarations and other block identifiers as " - " start_texts and end_texts." + " Accepts an array of `show` objects, each with file_path, start_text, end_text." + " 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 and" + " symbols.Special markers @000 and 000@ represent the file boundaries and can be" + " used for start_text and end_text for the first and last lines of the file" + " respectively. Avoid using both of the special markers together on non-empty" + " files. Never use content hashes as the start_text and end_text values. Do not use" + " the same pattern for the start_text and end_text. It is best to use function" + " names, variable declarations and other block identifiers as start_texts and" + " end_texts." ), "parameters": { "type": "object", @@ -127,7 +126,10 @@ def execute(cls, coder, show, **kwargs): error_outputs.append( cls.format_error( coder, - f"Show operation {show_index + 1}: Provide both 'start_text' and 'end_text'.", + ( + f"Show operation {show_index + 1}: Provide both 'start_text' and" + " 'end_text'." + ), file_path, start_text, end_text, @@ -284,7 +286,10 @@ def execute(cls, coder, show, **kwargs): error_outputs.append( cls.format_error( coder, - f"End pattern '{end_text}' not found in {file_path}. Do not search for it again.", + ( + f"End pattern '{end_text}' not found in {file_path}. Do not" + " search for it again." + ), file_path, start_text, end_text, @@ -297,7 +302,10 @@ def execute(cls, coder, show, **kwargs): error_outputs.append( cls.format_error( coder, - f"End pattern '{end_text}' not found after start pattern in {file_path}.", + ( + f"End pattern '{end_text}' not found after start pattern in" + f" {file_path}." + ), file_path, start_text, end_text, @@ -314,7 +322,7 @@ def execute(cls, coder, show, **kwargs): cls.format_error( coder, ( - f"Special markers cannot be used for ranges greater than 200 lines." + "Special markers cannot be used for ranges greater than 200 lines." f" The resolved range is {e_idx - s_idx + 1} lines." " Pick more refined boundaries." ), @@ -466,7 +474,8 @@ def execute(cls, coder, show, **kwargs): ) if already_up_to_details: coder.io.tool_output( - f"Lines already up to date in context for {len(already_up_to_details)} operation(s)" + "Lines already up to date in context for" + f" {len(already_up_to_details)} operation(s)" ) detail_str = "\n".join(already_up_to_details) From d4f892c82e8747a150c867c779386a524c9978d1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 10 May 2026 15:02:13 -0700 Subject: [PATCH 09/16] cli-12: 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 afcae12a9d7..922d5328671 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -1142,8 +1142,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 6d6be87bc8c..5ff6a5cd4e9 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", [])) @@ -197,11 +198,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 90622d2c26a0ccb27794dda139eb8293441fb0ec Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 10 May 2026 18:12:20 -0400 Subject: [PATCH 10/16] Fix observation manager usage in commands --- cecli/commands/clear.py | 3 ++- cecli/commands/reset.py | 3 ++- tests/helpers/observations/test_observation_manager.py | 2 -- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cecli/commands/clear.py b/cecli/commands/clear.py index c12449d1d98..f84567684d8 100644 --- a/cecli/commands/clear.py +++ b/cecli/commands/clear.py @@ -2,6 +2,7 @@ from cecli.commands.utils.base_command import BaseCommand from cecli.commands.utils.helpers import format_command_result +from cecli.helpers.observations.manager import ObservationManager class ClearCommand(BaseCommand): @@ -19,7 +20,7 @@ async def execute(cls, io, coder, args, **kwargs): ConversationService.get_manager(coder).clear_tag(MessageTag.FILE_CONTEXTS) ConversationService.get_files(coder).reset() - coder.observation_manager.reset() + ObservationManager.get_instance(coder).reset() # Clear TUI output if available if coder.tui and coder.tui(): diff --git a/cecli/commands/reset.py b/cecli/commands/reset.py index 2054b12b92e..fc6e64b0377 100644 --- a/cecli/commands/reset.py +++ b/cecli/commands/reset.py @@ -3,6 +3,7 @@ from cecli.commands.utils.base_command import BaseCommand from cecli.commands.utils.helpers import format_command_result from cecli.helpers.conversation import ConversationService +from cecli.helpers.observations.manager import ObservationManager class ResetCommand(BaseCommand): @@ -24,7 +25,7 @@ async def execute(cls, io, coder, args, **kwargs): # Re-initialize Conversation components with current coder ConversationService.get_manager(coder).initialize(reformat=True) ConversationService.get_files(coder) # Ensure instance exists/initialized - coder.observation_manager.reset() + ObservationManager.get_instance(coder).reset() # Clear TUI output if available if coder.tui and coder.tui(): diff --git a/tests/helpers/observations/test_observation_manager.py b/tests/helpers/observations/test_observation_manager.py index e19dc7996a6..19eb60ac3bf 100644 --- a/tests/helpers/observations/test_observation_manager.py +++ b/tests/helpers/observations/test_observation_manager.py @@ -71,7 +71,6 @@ async def test_compact_context_with_observations(): # Mock observation manager with some observations obs_manager = ObservationManager.get_instance(coder) obs_manager.observations = ["Observation 1"] - coder.observation_manager = obs_manager # Mock prompts coder.gpt_prompts = MagicMock() @@ -136,7 +135,6 @@ async def test_compact_context_with_observations_integration(): # Mock observation manager with some observations obs_manager = ObservationManager.get_instance(coder) obs_manager.observations = ["Observation 1"] - coder.observation_manager = obs_manager # Mock prompts coder.gpt_prompts = MagicMock() From bceb0b5b5716ff4f3a35141b2d73a81fafc36513 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 10 May 2026 19:13:29 -0700 Subject: [PATCH 11/16] cli-12: fix --- cecli/io.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/cecli/io.py b/cecli/io.py index cd7994a64e0..cbea56e2109 100644 --- a/cecli/io.py +++ b/cecli/io.py @@ -1707,13 +1707,24 @@ def get_default_notification_command(self): return f"zenity --notification --text='{NOTIFICATION_MESSAGE}'" return None # No known notification tool found elif system == "Windows": - # PowerShell MessageBox notification (no terminal window shown) - return ( - "powershell -Command" - " \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms');" - f" [System.Windows.Forms.MessageBox]::Show('{NOTIFICATION_MESSAGE}'," - " 'cecli')\"" + # PowerShell toast notification + ps_command = ( + ' "try {{ Add-Type -AssemblyName System.Runtime.WindowsRuntime; $null =' + " [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications," + " ContentType = WindowsRuntime] }} catch {{}}; " + "$template =" + " [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent" + "([Windows.UI.Notifications.ToastTemplateType]::ToastText02); " + "$toastXml = $template.GetXml(); " + "$toastXml.GetElementsByTagName('text')[0].AppendChild" + "($template.CreateTextNode('cecli')) > $null; " + f"$toastXml.GetElementsByTagName('text')[1].AppendChild" + f"($template.CreateTextNode('{NOTIFICATION_MESSAGE}')) > $null; " + "$toast = [Windows.UI.Notifications.ToastNotification]::new($toastXml); " + "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('cecli')" + '.Show($toast)"' ) + return "powershell -WindowStyle Hidden -Command" + ps_command return None # Unknown system From b77e031cd0c38a36a4a56f39e4c8922d871c5725 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 11 May 2026 22:05:37 -0700 Subject: [PATCH 12/16] cli-17: formatted --- cecli/coders/agent_coder.py | 25 +++++++++++++-------- cecli/io.py | 2 +- cecli/tools/context_manager.py | 3 +-- cecli/tools/read_range.py | 41 +++++++++++++++++++++------------- tests/basic/test_models.py | 18 +++++++++------ 5 files changed, 54 insertions(+), 35 deletions(-) diff --git a/cecli/coders/agent_coder.py b/cecli/coders/agent_coder.py index 77db195c201..8524d707185 100644 --- a/cecli/coders/agent_coder.py +++ b/cecli/coders/agent_coder.py @@ -282,9 +282,7 @@ async def _exec_async(): content_parts.append(item.text) return "".join(content_parts) except Exception as e: - self.io.tool_warning( - (f"Executing {tool_name} on {server.name} failed:\nError: {e}") - ) + self.io.tool_warning(f"Executing {tool_name} on {server.name} failed:\nError: {e}") return f"Error executing tool call {tool_name}: {e}" result, interrupted = await interruptible(_exec_async(), self.interrupt_event) @@ -381,7 +379,10 @@ def get_context_symbol_outline(self): try: result = '\n' result += "## Symbol Outline (Current Context)\n\n" - result += "Code definitions (classes, functions, methods, etc.) found in files currently in chat context." + result += ( + "Code definitions (classes, functions, methods, etc.) found in files currently in" + " chat context." + ) result += "\n\n" files_to_outline = list(self.abs_fnames) + list(self.abs_read_only_fnames) if not files_to_outline: @@ -522,7 +523,10 @@ def get_context_summary(self): ) if editable_files: result += "\n".join(editable_files) + "\n\n" - result += f"**Total editable: {len(editable_files)} files, {editable_tokens:,} tokens**\n\n" + result += ( + f"**Total editable: {len(editable_files)} files," + f" {editable_tokens:,} tokens**\n\n" + ) else: result += "No editable files in context\n\n" if self.abs_read_only_fnames: @@ -542,7 +546,10 @@ def get_context_summary(self): ) if readonly_files: result += "\n".join(readonly_files) + "\n\n" - result += f"**Total read-only: {len(readonly_files)} files, {readonly_tokens:,} tokens**\n\n" + result += ( + f"**Total read-only: {len(readonly_files)} files," + f" {readonly_tokens:,} tokens**\n\n" + ) else: result += "No read-only files in context\n\n" extra_tokens = sum(self.context_block_tokens.values()) @@ -730,7 +737,7 @@ async def gather_and_await(): self.model_kwargs = {} result_message = f"Error executing {tool_name}: {e}" self.io.tool_error( - (f"Error during {tool_name} execution: {e}\n{traceback.format_exc()}") + f"Error during {tool_name} execution: {e}\n{traceback.format_exc()}" ) tool_responses.append( {"role": "tool", "tool_call_id": tool_call.id, "content": result_message} @@ -1020,8 +1027,8 @@ def _generate_tool_context(self, repetitive_tools): context_parts.append("## File Editing Tools Disabled") context_parts.append( "File editing tools are currently disabled. Use `ReadRange` to determine the" - " current content hash prefixes needed to perform an edit and activate them when you" - " are ready to edit a file." + " current content hash prefixes needed to perform an edit and activate them when" + " you are ready to edit a file." ) context_parts.append("\n\n") diff --git a/cecli/io.py b/cecli/io.py index 230b87f0745..ef439eba6a6 100644 --- a/cecli/io.py +++ b/cecli/io.py @@ -1718,7 +1718,7 @@ def get_default_notification_command(self): "$toastXml = $template.GetXml(); " "$toastXml.GetElementsByTagName('text')[0].AppendChild" "($template.CreateTextNode('cecli')) > $null; " - f"$toastXml.GetElementsByTagName('text')[1].AppendChild" + "$toastXml.GetElementsByTagName('text')[1].AppendChild" f"($template.CreateTextNode('{NOTIFICATION_MESSAGE}')) > $null; " "$toast = [Windows.UI.Notifications.ToastNotification]::new($toastXml); " "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('cecli')" diff --git a/cecli/tools/context_manager.py b/cecli/tools/context_manager.py index f969f48edcc..de565573f15 100644 --- a/cecli/tools/context_manager.py +++ b/cecli/tools/context_manager.py @@ -32,8 +32,7 @@ class Tool(BaseTool): "type": "array", "items": {"type": "string"}, "description": ( - "List of file paths to add as read-only. " - "Limit to at most 2 at a time." + "List of file paths to add as read-only. Limit to at most 2 at a time." ), }, "create": { diff --git a/cecli/tools/read_range.py b/cecli/tools/read_range.py index 0b0310ab997..66cafedd73b 100644 --- a/cecli/tools/read_range.py +++ b/cecli/tools/read_range.py @@ -22,17 +22,16 @@ class Tool(BaseTool): "name": "ReadRange", "description": ( "Get content hash prefixes of content between start and end patterns in files." - " Accepts an array of `show` objects, each with file_path, start_text," - " end_text. 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 and symbols." - "Special markers @000 and 000@ represent the file boundaries and can be" - " used for start_text and end_text for the first and last lines of" - " the file respectively. Avoid using both of the special markers together on non-empty files." - " Never use content hashes as the start_text and end_text values." - " Do not use the same pattern for the start_text and end_text." - " It is best to use function names, variable declarations and other block identifiers as " - " start_texts and end_texts." + " Accepts an array of `show` objects, each with file_path, start_text, end_text." + " 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 and" + " symbols.Special markers @000 and 000@ represent the file boundaries and can be" + " used for start_text and end_text for the first and last lines of the file" + " respectively. Avoid using both of the special markers together on non-empty" + " files. Never use content hashes as the start_text and end_text values. Do not use" + " the same pattern for the start_text and end_text. It is best to use function" + " names, variable declarations and other block identifiers as start_texts and" + " end_texts." ), "parameters": { "type": "object", @@ -127,7 +126,10 @@ def execute(cls, coder, show, **kwargs): error_outputs.append( cls.format_error( coder, - f"Show operation {show_index + 1}: Provide both 'start_text' and 'end_text'.", + ( + f"Show operation {show_index + 1}: Provide both 'start_text' and" + " 'end_text'." + ), file_path, start_text, end_text, @@ -284,7 +286,10 @@ def execute(cls, coder, show, **kwargs): error_outputs.append( cls.format_error( coder, - f"End pattern '{end_text}' not found in {file_path}. Do not search for it again.", + ( + f"End pattern '{end_text}' not found in {file_path}. Do not" + " search for it again." + ), file_path, start_text, end_text, @@ -297,7 +302,10 @@ def execute(cls, coder, show, **kwargs): error_outputs.append( cls.format_error( coder, - f"End pattern '{end_text}' not found after start pattern in {file_path}.", + ( + f"End pattern '{end_text}' not found after start pattern in" + f" {file_path}." + ), file_path, start_text, end_text, @@ -314,7 +322,7 @@ def execute(cls, coder, show, **kwargs): cls.format_error( coder, ( - f"Special markers cannot be used for ranges greater than 200 lines." + "Special markers cannot be used for ranges greater than 200 lines." f" The resolved range is {e_idx - s_idx + 1} lines." " Pick more refined boundaries." ), @@ -466,7 +474,8 @@ def execute(cls, coder, show, **kwargs): ) if already_up_to_details: coder.io.tool_output( - f"Lines already up to date in context for {len(already_up_to_details)} operation(s)" + "Lines already up to date in context for" + f" {len(already_up_to_details)} operation(s)" ) detail_str = "\n".join(already_up_to_details) diff --git a/tests/basic/test_models.py b/tests/basic/test_models.py index 9138d49f37f..5a9e5171d36 100644 --- a/tests/basic/test_models.py +++ b/tests/basic/test_models.py @@ -818,9 +818,11 @@ async def test_tool_description_escaping(self, mock_acompletion): """ model = Model("gpt-4") messages = [{"role": "user", "content": "Hello"}] - + # A complex description with various special characters - complex_description = 'This is a "test" description with `special` characters like \\, \n, and *.' + complex_description = ( + 'This is a "test" description with `special` characters like \\, \n, and *.' + ) # Mock tool with the complex description mock_tool = { @@ -840,21 +842,23 @@ async def test_tool_description_escaping(self, mock_acompletion): # Verify that acompletion was called mock_acompletion.assert_called_once() - + # Get the keyword arguments passed to acompletion call_kwargs = mock_acompletion.call_args.kwargs - + # Check that the 'tools' argument is present and correctly formatted assert "tools" in call_kwargs sent_tools = call_kwargs["tools"] assert isinstance(sent_tools, list) assert len(sent_tools) == 1 - + # Verify the description of the sent tool sent_tool_function = sent_tools[0].get("function", {}) sent_description = sent_tool_function.get("description") - + # The description should be a JSON-escaped string # Expected: 'This is a \\"test\\" description with `special` characters like \\\\, \\n, and *.' - expected_escaped_description = 'This is a \\"test\\" description with `special` characters like \\\\, \\n, and *.' + expected_escaped_description = ( + 'This is a \\"test\\" description with `special` characters like \\\\, \\n, and *.' + ) assert sent_description == expected_escaped_description From 3ab3d86f1a60b017ee0a220414d6d7454aa5c6ee Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 12 May 2026 15:28:47 -0700 Subject: [PATCH 13/16] fix: Remove unconditional bell ring in tool call info printing Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/coders/base_coder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 922d5328671..0a6678472fb 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -2780,7 +2780,6 @@ async def process_tool_calls(self, tool_call_response): def _print_tool_call_info(self, server_tool_calls): """Print information about an MCP tool call.""" - self.io.ring_bell() # self.io.tool_output("Preparing to run MCP tools", bold=False) for server, tool_calls in server_tool_calls.items(): From 60c81d9b9c6feb6569906f08fa6b6df1da7072a2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 12 May 2026 19:30:40 -0400 Subject: [PATCH 14/16] Ask about auto loading before doing it --- cecli/main.py | 22 ++++++++++++++-------- cecli/sessions.py | 4 ++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/cecli/main.py b/cecli/main.py index 0549ea78b55..89f637e0910 100644 --- a/cecli/main.py +++ b/cecli/main.py @@ -1194,15 +1194,21 @@ def get_io(pretty): if args.exit: return await graceful_exit(coder) if args.auto_load: - try: - from cecli.sessions import SessionManager + if await pre_init_io.confirm_ask( + "Do you want to load your previous session?", + acknowledge=True, + explicit_yes_required=True, + ): + try: + from cecli.sessions import SessionManager - session_manager = SessionManager(coder, io) - await session_manager.load_session( - args.auto_save_session_name if args.auto_save_session_name else "auto-save" - ) - except Exception: - pass + session_manager = SessionManager(coder, io) + await session_manager.load_session( + args.auto_save_session_name if args.auto_save_session_name else "auto-save", + switch=False, + ) + except Exception: + pass if suppress_pre_init: await graceful_exit(coder) diff --git a/cecli/sessions.py b/cecli/sessions.py index 5ff6a5cd4e9..f1ee5a12570 100644 --- a/cecli/sessions.py +++ b/cecli/sessions.py @@ -88,7 +88,7 @@ def list_sessions(self) -> List[Dict]: return sessions - async def load_session(self, session_identifier: str) -> bool: + async def load_session(self, session_identifier: str, switch=True) -> 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.") @@ -113,7 +113,7 @@ async def load_session(self, session_identifier: str) -> bool: # Apply session data applied, loaded_edit_format = await self._apply_session_data(session_data, session_file) - if applied: + if applied and switch: from cecli.commands import SwitchCoderSignal edit_format_to_switch_to = self.coder.edit_format From 363541beaf4200becf2ae2c8226edaaaa8bb7edd Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 12 May 2026 19:46:53 -0400 Subject: [PATCH 15/16] Remove extraneous space in schema --- cecli/tools/read_range.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cecli/tools/read_range.py b/cecli/tools/read_range.py index 66cafedd73b..822a2e9aeba 100644 --- a/cecli/tools/read_range.py +++ b/cecli/tools/read_range.py @@ -30,7 +30,7 @@ class Tool(BaseTool): " respectively. Avoid using both of the special markers together on non-empty" " files. Never use content hashes as the start_text and end_text values. Do not use" " the same pattern for the start_text and end_text. It is best to use function" - " names, variable declarations and other block identifiers as start_texts and" + " names, variable declarations and other block identifiers as start_texts and" " end_texts." ), "parameters": { From be784ab3b6d93bed98e3d4440bf0056706a544e4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 12 May 2026 21:34:31 -0400 Subject: [PATCH 16/16] #512: General fix for tool escaping but just once on registration --- cecli/tools/edit_text.py | 6 +++--- cecli/tools/load_skill.py | 5 +---- cecli/tools/remove_skill.py | 5 +---- cecli/tools/thinking.py | 4 ++-- cecli/tools/utils/registry.py | 11 +++++++++++ 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/cecli/tools/edit_text.py b/cecli/tools/edit_text.py index 7004e46d055..df6c8fc8c56 100644 --- a/cecli/tools/edit_text.py +++ b/cecli/tools/edit_text.py @@ -37,7 +37,7 @@ class Tool(BaseTool): "Can handle an array of up to 10 edits across multiple files. " "Each edit must include its own file_path and operation type. " "Use content hash ranges with the start_line and end_line parameters with format " - '"{4 char hash}" (without the braces). For empty files, use "@000" as the ' + "`{4 char hash}` (without the braces). For empty files, use `@000` as the " "content hash references." ), "parameters": { @@ -71,14 +71,14 @@ class Tool(BaseTool): "start_line": { "type": "string", "description": ( - 'Content hash for start line: "{4 char hash}" (without ' + "Content hash for start line: `{4 char hash}` (without " "the braces)" ), }, "end_line": { "type": "string", "description": ( - 'Content hash for end line: "{4 char hash}" (without the' + "Content hash for end line: `{4 char hash}` (without the" " braces)" ), }, diff --git a/cecli/tools/load_skill.py b/cecli/tools/load_skill.py index e8a6d4f5e4e..15f620579fb 100644 --- a/cecli/tools/load_skill.py +++ b/cecli/tools/load_skill.py @@ -7,10 +7,7 @@ class Tool(BaseTool): "type": "function", "function": { "name": "LoadSkill", - "description": ( - "Load a skill by name (agent mode only). Adds skill to include list and removes" - " from exclude list." - ), + "description": "Load a skill by name.", "parameters": { "type": "object", "properties": { diff --git a/cecli/tools/remove_skill.py b/cecli/tools/remove_skill.py index feb2ae6e9de..70afb02ebfd 100644 --- a/cecli/tools/remove_skill.py +++ b/cecli/tools/remove_skill.py @@ -7,10 +7,7 @@ class Tool(BaseTool): "type": "function", "function": { "name": "RemoveSkill", - "description": ( - "Remove a skill by name (agent mode only). Removes skill from include list and adds" - " to exclude list." - ), + "description": "Remove a skill by name.", "parameters": { "type": "object", "properties": { diff --git a/cecli/tools/thinking.py b/cecli/tools/thinking.py index 9e40c3fe311..05a2ffa239b 100644 --- a/cecli/tools/thinking.py +++ b/cecli/tools/thinking.py @@ -11,8 +11,8 @@ class Tool(BaseTool): "function": { "name": "Thinking", "description": ( - "Use this tool to store useful facts for later " - "keep a scratch pad of your current efforts " + "Use this tool to store useful facts for later, " + "keep a scratch pad of your current efforts, " "and clarify your thoughts and intentions for your next steps." ), "parameters": { diff --git a/cecli/tools/utils/registry.py b/cecli/tools/utils/registry.py index 8c7e74706b9..45f333301ff 100644 --- a/cecli/tools/utils/registry.py +++ b/cecli/tools/utils/registry.py @@ -6,6 +6,7 @@ based on agent configuration. """ +import json import traceback from pathlib import Path from typing import Dict, List, Optional, Set, Type @@ -32,6 +33,16 @@ def register(cls, tool_class): except Exception: pass + description = tool_class.SCHEMA["function"]["description"] + wrapped = f'"{description}"' + + try: + json.loads(wrapped) + except json.JSONDecodeError: + tool_class.SCHEMA["function"]["description"] = json.dumps( + description, ensure_ascii=False + )[1:-1] + if not name and hasattr(tool_class, "NORM_NAME"): name = tool_class.NORM_NAME