From 0caaf6166acc233a2be8c734ffa06fffa27813fe Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Tue, 27 Jan 2026 01:09:26 -0500 Subject: [PATCH 1/7] Bump Version --- cecli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cecli/__init__.py b/cecli/__init__.py index 8fc1c520701..21130385bd7 100644 --- a/cecli/__init__.py +++ b/cecli/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.96.4.dev" +__version__ = "0.96.5.dev" safe_version = __version__ try: From ee2de9bdff6c262da68c5e4c892dac9b922b56c9 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Tue, 27 Jan 2026 03:46:13 -0500 Subject: [PATCH 2/7] Auto complete works on empty input --- cecli/io.py | 54 ++++++++++++++++++++++++++++++++++++------------ cecli/tui/app.py | 29 ++++++++++++++++++++------ 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/cecli/io.py b/cecli/io.py index 4ee3ee21322..5a4144d7282 100644 --- a/cecli/io.py +++ b/cecli/io.py @@ -148,7 +148,6 @@ def __init__( self.rel_fnames = rel_fnames self.encoding = encoding self.abs_read_only_fnames = abs_read_only_fnames or [] - self.post_filter_commands = ["/add"] fname_to_rel_fnames = defaultdict(list) for rel_fname in addable_rel_fnames: @@ -214,22 +213,47 @@ def tokenize(self): def get_command_completions(self, document, complete_event, text, words): if len(words) == 1 and not text[-1].isspace(): + # Handle command completion (e.g., typing "/ad" should complete to "/add") partial = words[0].lower() + # Strip leading '/' if present for comparison with command names + if partial.startswith("/"): + partial = partial[1:] candidates = [cmd for cmd in self.command_names if cmd.startswith(partial)] for candidate in sorted(candidates): - yield Completion(candidate, start_position=-len(words[-1])) + # Add back the leading '/' for the completion + yield Completion("/" + candidate, start_position=-len(words[-1])) return - if len(words) <= 1 or text[-1].isspace(): - return + # Handle command followed by space: trigger auto-completion with empty partial + if text[-1].isspace(): + # We have a command followed by space, trigger auto-completion with empty string + if len(words) == 1: + # Command with no arguments yet, just a trailing space + partial = "" + # We need to get the command name without the trailing space + # The command is words[0] but might have leading '/' + cmd_text = words[0] + else: + # Command with arguments and trailing space + partial = "" + cmd_text = text.rstrip() # Remove trailing space for matching + else: + # No trailing space + if len(words) <= 1: + return + partial = words[-1].lower() + cmd_text = text - cmd = words[0] - partial = words[-1].lower() + # Pass the text (without trailing space if present) to matching_commands + matches, matched_cmd, _ = self.commands.matching_commands(cmd_text.rstrip()) + if not matches: + return - matches, _, _ = self.commands.matching_commands(cmd) if len(matches) == 1: cmd = matches[0] - elif cmd not in matches: + elif matched_cmd in matches: + cmd = matched_cmd + else: return raw_completer = self.commands.get_raw_completions(cmd) @@ -242,11 +266,14 @@ def get_command_completions(self, document, complete_event, text, words): if candidates is None: return - if cmd in self.post_filter_commands: - candidates = [word for word in candidates if partial in word.lower()] + candidates = [word for word in candidates if partial in word.lower()] for candidate in sorted(candidates): - yield Completion(candidate, start_position=-len(words[-1])) + # Calculate start position based on partial, not words[-1] + # When partial is empty (trailing space), start_position should be 0 + # When partial is not empty, replace that many characters + start_position = -len(partial) if partial else 0 + yield Completion(candidate, start_position=start_position) def get_completions(self, document, complete_event): self.tokenize() @@ -256,8 +283,9 @@ def get_completions(self, document, complete_event): if not words: return - if text and text[-1].isspace(): - # don't keep completing after a space + if text and text[-1].isspace() and not text.startswith("/"): + # don't keep completing after a space for non-commands + # For commands, we want to allow completion with empty string partial return if text[0] == "/": diff --git a/cecli/tui/app.py b/cecli/tui/app.py index 2155e7cac9a..01dcea809bb 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -884,7 +884,9 @@ def _get_suggestions(self, text: str) -> list[str]: suggestions = [] commands = self.worker.coder.commands - if len(text) and text[-1] == " ": + # Only return early for non-commands ending with space + # For commands, we want to allow completion with empty string partial + if len(text) and text[-1] == " " and not text.startswith("/"): return if "@" in text: @@ -905,12 +907,21 @@ def _get_suggestions(self, text: str) -> list[str]: suggestions = all_commands else: suggestions = [c for c in all_commands if c.startswith(cmd_part)] - elif len(parts) > 1: + else: # Complete command argument + # This handles both: + # 1. len(parts) > 1: command with arguments + # 2. len(parts) == 1 and text.endswith(" "): command with trailing space cmd_name = cmd_part - end_lookup = text.rsplit(maxsplit=1) - arg_prefix = end_lookup[-1] + if text.endswith(" "): + # Command with trailing space, empty argument prefix + arg_prefix = "" + else: + # Get the last word as argument prefix + end_lookup = text.rsplit(maxsplit=1) + arg_prefix = end_lookup[-1] + arg_prefix_lower = arg_prefix.lower() # Check if this command needs path-based completion @@ -955,8 +966,14 @@ def _get_completed_text(self, current_text: str, completion: str) -> str: """Calculate the new text after applying completion.""" if current_text.startswith("/"): parts = current_text.rsplit(maxsplit=1) - if len(parts) == 1: - # Replace entire command + + # Check if we have a command with trailing space + # This is when we want to insert argument completions after the space + if len(parts) == 1 and current_text.endswith(" "): + # Command with trailing space, insert completion after space + return current_text + completion + elif len(parts) == 1: + # Replace entire command (command name completion) # Only add space if command takes arguments commands = self.worker.coder.commands try: From ab5b4099c39df8a0f2983bafe23720a9470e2bd2 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Tue, 27 Jan 2026 03:53:42 -0500 Subject: [PATCH 3/7] Agent mode should end itself and not reflect when it marks self as finished --- cecli/coders/agent_coder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cecli/coders/agent_coder.py b/cecli/coders/agent_coder.py index 86ba929465d..b02be54c101 100644 --- a/cecli/coders/agent_coder.py +++ b/cecli/coders/agent_coder.py @@ -724,6 +724,7 @@ async def reply_completed(self): ) = await self._process_tool_commands(content) if self.agent_finished: self.tool_usage_history = [] + self.reflected_message = None if self.files_edited_by_tools: _ = await self.auto_commit(self.files_edited_by_tools) return False From e8cdd57e795e6bdd4cef0031f9ad20aa4ccff6de Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Tue, 27 Jan 2026 08:27:27 -0500 Subject: [PATCH 4/7] Remind editor model that it actually has to apply the edits from the architect model --- cecli/coders/architect_coder.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cecli/coders/architect_coder.py b/cecli/coders/architect_coder.py index aa5789e1b17..8ce450b3259 100644 --- a/cecli/coders/architect_coder.py +++ b/cecli/coders/architect_coder.py @@ -61,6 +61,14 @@ async def reply_completed(self): if self.verbose: editor_coder.show_announcements() + postamble = """ + The above changes are proposed changes. + You must repeat SEARCH/REPLACE blocks in order to apply edits. + Shell commands must also be duplicated in order to run them. + """ + + content = f"Please implement all requested changes from:\n{content}\n{postamble}" + try: await editor_coder.generate(user_message=content, preproc=False) From 4af3b9ffc0073a7392a915466c0ebd404dbe2a12 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Tue, 27 Jan 2026 08:45:18 -0500 Subject: [PATCH 5/7] Fix extra-prepended slash in io.py auto completer --- cecli/io.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cecli/io.py b/cecli/io.py index 5a4144d7282..f7f86f14b26 100644 --- a/cecli/io.py +++ b/cecli/io.py @@ -218,10 +218,11 @@ def get_command_completions(self, document, complete_event, text, words): # Strip leading '/' if present for comparison with command names if partial.startswith("/"): partial = partial[1:] - candidates = [cmd for cmd in self.command_names if cmd.startswith(partial)] + # Compare with command names without leading '/' + candidates = [cmd for cmd in self.command_names if cmd[1:].startswith(partial)] for candidate in sorted(candidates): # Add back the leading '/' for the completion - yield Completion("/" + candidate, start_position=-len(words[-1])) + yield Completion(candidate, start_position=-len(words[-1])) return # Handle command followed by space: trigger auto-completion with empty partial From 585839e51c911c541421446372e047380878d1cc Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Tue, 27 Jan 2026 13:22:41 -0500 Subject: [PATCH 6/7] Add more autosave check points that force update the coder (on SwitchCoder, SystemExit, reflected message and turn of run loop) --- cecli/coders/base_coder.py | 7 +++++-- cecli/main.py | 2 ++ cecli/tui/worker.py | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 611d6a0c84e..923d5b56780 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -1584,6 +1584,7 @@ async def run_one(self, user_message, preproc): pass if not self.reflected_message: + await self.auto_save_session(force=True) break if self.num_reflections >= self.max_reflections: @@ -1603,6 +1604,8 @@ async def run_one(self, user_message, preproc): if self.enable_context_compaction: await self.compact_context_if_needed() + await self.auto_save_session(force=True) + async def check_and_open_urls(self, exc, friendly_msg=None): """Check exception for URLs, offer to open in a browser, with user-friendly error msgs.""" text = str(exc) @@ -3776,7 +3779,7 @@ def apply_edits(self, edits): def apply_edits_dry_run(self, edits): return edits - async def auto_save_session(self): + async def auto_save_session(self, force=False): """Automatically save the current session to {auto-save-session-name}.json.""" if not getattr(self.args, "auto_save", False): return @@ -3793,7 +3796,7 @@ async def auto_save_session(self): # Throttle autosave to run at most once every 15 seconds current_time = time.time() - if current_time - self._last_autosave_time >= 15.0: + if current_time - self._last_autosave_time >= 15.0 or force: try: self._last_autosave_time = current_time session_manager = SessionManager(self, self.io) diff --git a/cecli/main.py b/cecli/main.py index 4ab8833cb57..b55c816c270 100644 --- a/cecli/main.py +++ b/cecli/main.py @@ -1202,6 +1202,7 @@ def apply_model_overrides(model_name): return await graceful_exit(coder) except SwitchCoderSignal as switch: coder.ok_to_warm_cache = False + await coder.auto_save_session(force=True) if hasattr(switch, "placeholder") and switch.placeholder is not None: io.placeholder = switch.placeholder @@ -1229,6 +1230,7 @@ def apply_model_overrides(model_name): except SystemExit: sys.settrace(None) + await coder.auto_save_session(force=True) return await graceful_exit(coder) diff --git a/cecli/tui/worker.py b/cecli/tui/worker.py index cfa2acc0a42..f42ec284d6f 100644 --- a/cecli/tui/worker.py +++ b/cecli/tui/worker.py @@ -95,6 +95,7 @@ async def _async_run(self): except SwitchCoderSignal as switch: # Handle chat mode switches (e.g., /chat-mode architect) try: + await self.coder.auto_save_session(force=True) kwargs = dict(io=self.coder.io, from_coder=self.coder) kwargs.update(switch.kwargs) if "show_announcements" in kwargs: From 0b494d09866b3d9ca9862886d05c2e28a2edf06f Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Tue, 27 Jan 2026 13:26:54 -0500 Subject: [PATCH 7/7] Await autosave future as well, so force parameter always results in an auto-save write --- cecli/coders/base_coder.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 923d5b56780..330a22ce2c6 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -3792,7 +3792,13 @@ async def auto_save_session(self, force=False): self._autosave_future = None if self._autosave_future and not self._autosave_future.done(): - return + if force: + try: + await self._autosave_future + except Exception: + pass + else: + return # Throttle autosave to run at most once every 15 seconds current_time = time.time()