diff --git a/cecli/__init__.py b/cecli/__init__.py index 8afa159985f..6991056b34c 100644 --- a/cecli/__init__.py +++ b/cecli/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.96.2.dev" +__version__ = "0.96.3.dev" safe_version = __version__ try: diff --git a/cecli/coders/agent_coder.py b/cecli/coders/agent_coder.py index db107fc2544..86ba929465d 100644 --- a/cecli/coders/agent_coder.py +++ b/cecli/coders/agent_coder.py @@ -799,11 +799,9 @@ async def reply_completed(self): saved_message = await self.auto_commit(self.files_edited_by_tools) if not saved_message and hasattr(self.gpt_prompts, "files_content_gpt_edits_no_repo"): saved_message = self.gpt_prompts.files_content_gpt_edits_no_repo - self.move_back_cur_messages(saved_message) self.tool_call_count = 0 self.files_added_in_exploration = set() self.files_edited_by_tools = set() - self.move_back_cur_messages(None) return False async def _execute_tool_with_registry(self, norm_tool_name, params): diff --git a/cecli/coders/architect_coder.py b/cecli/coders/architect_coder.py index 251e25d3bf3..507faed0603 100644 --- a/cecli/coders/architect_coder.py +++ b/cecli/coders/architect_coder.py @@ -52,12 +52,8 @@ async def reply_completed(self): editor_coder = await Coder.create(**new_kwargs) - # Clear ALL messages for editor coder (start fresh) - ConversationManager.reset() - # Re-initialize ConversationManager with editor coder - ConversationManager.initialize(editor_coder) - ConversationManager.clear_cache() + ConversationManager.initialize(editor_coder, reset=True, reformat=True) if self.verbose: editor_coder.show_announcements() @@ -69,48 +65,45 @@ async def reply_completed(self): editor_all_messages = ConversationManager.get_messages() # Clear manager and restore original state - ConversationManager.reset() - ConversationManager.initialize(original_coder or self) + ConversationManager.initialize(original_coder or self, reset=True, reformat=True) # Restore original messages with all metadata for msg in original_all_messages: - ConversationManager.add_message( - msg.to_dict(), - MessageTag(msg.tag), - priority=msg.priority, - timestamp=msg.timestamp, - mark_for_delete=msg.mark_for_delete, - hash_key=msg.hash_key, - ) + if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: + ConversationManager.add_message( + message_dict=msg.message_dict, + tag=MessageTag(msg.tag), + priority=msg.priority, + mark_for_delete=msg.mark_for_delete, + force=True, + ) # Append editor's DONE and CUR messages (but not other tags like SYSTEM) for msg in editor_all_messages: if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: ConversationManager.add_message( - msg.to_dict(), - MessageTag(msg.tag), + message_dict=msg.message_dict, + tag=MessageTag(msg.tag), priority=msg.priority, - timestamp=msg.timestamp, mark_for_delete=msg.mark_for_delete, - hash_key=msg.hash_key, + force=True, ) - self.move_back_cur_messages("I made those changes to the files.") self.total_cost = editor_coder.total_cost self.coder_commit_hashes = editor_coder.coder_commit_hashes except Exception as e: self.io.tool_error(e) # Restore original state on error - ConversationManager.reset() - ConversationManager.initialize(original_coder or self) + ConversationManager.initialize(original_coder or self, reset=True, reformat=True) + for msg in original_all_messages: - ConversationManager.add_message( - msg.to_dict(), - MessageTag(msg.tag), - priority=msg.priority, - timestamp=msg.timestamp, - mark_for_delete=msg.mark_for_delete, - hash_key=msg.hash_key, - ) + if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: + ConversationManager.add_message( + message_dict=msg.message_dict, + tag=MessageTag(msg.tag), + priority=msg.priority, + mark_for_delete=msg.mark_for_delete, + force=True, + ) raise SwitchCoderSignal(main_model=self.main_model, edit_format="architect") diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 5c88e2c901d..e725c4d0bff 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -1659,7 +1659,7 @@ def keyboard_interrupt(self): # Old summarization system removed - using context compaction logic instead - async def compact_context_if_needed(self, force=False): + async def compact_context_if_needed(self, force=False, message=""): if not self.enable_context_compaction: return @@ -1687,9 +1687,14 @@ async def compact_context_if_needed(self, force=False): # Check if done_messages alone exceed the limit if done_tokens > self.context_compaction_max_tokens or done_tokens > cur_tokens: # Create a summary of the done_messages + # Append custom message to compaction prompt if provided + compaction_prompt = self.gpt_prompts.compaction_prompt + if message: + compaction_prompt = f"{compaction_prompt}\n\n{message}" + summary_text = await self.summarizer.summarize_all_as_text( done_messages, - self.gpt_prompts.compaction_prompt, + compaction_prompt, self.context_compaction_summary_tokens, ) @@ -1719,9 +1724,14 @@ async def compact_context_if_needed(self, force=False): # Check if cur_messages alone exceed the limit (after potentially compacting done_messages) if cur_tokens > self.context_compaction_max_tokens or cur_tokens > done_tokens: # Create a summary of the cur_messages + # Append custom message to compaction prompt if provided + compaction_prompt = self.gpt_prompts.compaction_prompt + if message: + compaction_prompt = f"{compaction_prompt}\n\n{message}" + cur_summary_text = await self.summarizer.summarize_all_as_text( cur_messages, - self.gpt_prompts.compaction_prompt, + compaction_prompt, self.context_compaction_summary_tokens, ) @@ -1774,32 +1784,6 @@ async def compact_context_if_needed(self, force=False): self.io.tool_warning("Proceeding with full history for now.") return - def move_back_cur_messages(self, message): - # Move CUR messages to DONE in ConversationManager - # Get current CUR messages - cur_messages = ConversationManager.get_messages_dict(MessageTag.CUR) - - # Clear CUR messages from ConversationManager - ConversationManager.clear_tag(MessageTag.CUR) - - # Add them back as DONE messages - for msg in cur_messages: - ConversationManager.add_message( - message_dict=msg, - tag=MessageTag.DONE, - ) - - # TODO check for impact on image messages - if message: - ConversationManager.add_message( - message_dict=dict(role="user", content=message), - tag=MessageTag.DONE, - ) - ConversationManager.add_message( - message_dict=dict(role="assistant", content="Ok."), - tag=MessageTag.DONE, - ) - def normalize_language(self, lang_code): """ Convert a locale code such as ``en_US`` or ``fr`` into a readable @@ -2277,8 +2261,6 @@ async def send_message(self, inp): if not saved_message and hasattr(self.gpt_prompts, "files_content_gpt_edits_no_repo"): saved_message = self.gpt_prompts.files_content_gpt_edits_no_repo - self.move_back_cur_messages(saved_message) - if not interrupted: add_rel_files_message = await self.check_for_file_mentions(content) if add_rel_files_message: @@ -3783,8 +3765,6 @@ async def dirty_commit(self): await self.repo.commit(fnames=self.need_commit_before_edits, coder=self) - # files changed, move cur messages back behind the files messages - # self.move_back_cur_messages(self.gpt_prompts.files_content_local_edits) return True def get_edits(self, mode="update"): diff --git a/cecli/commands/compact.py b/cecli/commands/compact.py index 510d9215fab..ddedddf33b6 100644 --- a/cecli/commands/compact.py +++ b/cecli/commands/compact.py @@ -7,7 +7,9 @@ class CompactCommand(BaseCommand): @classmethod async def execute(cls, io, coder, args, **kwargs): - await coder.compact_context_if_needed(force=True) + # Pass args as message parameter if it's not empty + message = args.strip() if args else "" + await coder.compact_context_if_needed(force=True, message=message) @classmethod def get_help(cls) -> str: diff --git a/cecli/commands/editor_model.py b/cecli/commands/editor_model.py index 1d142899cc2..b2cc39c9081 100644 --- a/cecli/commands/editor_model.py +++ b/cecli/commands/editor_model.py @@ -62,12 +62,8 @@ async def execute(cls, io, coder, args, **kwargs): temp_coder = await Coder.create(**new_kwargs) - # Clear ALL messages for temp coder (start fresh) - ConversationManager.reset() - # Re-initialize ConversationManager with temp coder - ConversationManager.initialize(temp_coder) - ConversationManager.clear_cache() + ConversationManager.initialize(temp_coder, reset=True, reformat=True) verbose = kwargs.get("verbose", False) if verbose: @@ -82,37 +78,30 @@ async def execute(cls, io, coder, args, **kwargs): temp_all_messages = ConversationManager.get_messages() # Clear manager and restore original state - ConversationManager.reset() - ConversationManager.initialize(original_coder) + ConversationManager.initialize(original_coder, reset=True, reformat=True) # Restore original messages with all metadata for msg in original_all_messages: - ConversationManager.add_message( - msg.to_dict(), - MessageTag(msg.tag), - priority=msg.priority, - timestamp=msg.timestamp, - mark_for_delete=msg.mark_for_delete, - hash_key=msg.hash_key, - ) + if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: + ConversationManager.add_message( + message_dict=msg.message_dict, + tag=MessageTag(msg.tag), + priority=msg.priority, + mark_for_delete=msg.mark_for_delete, + force=True, + ) # Append temp coder's DONE and CUR messages (but not other tags like SYSTEM) for msg in temp_all_messages: if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: ConversationManager.add_message( - msg.to_dict(), - MessageTag(msg.tag), + message_dict=msg.message_dict, + tag=MessageTag(msg.tag), priority=msg.priority, - timestamp=msg.timestamp, mark_for_delete=msg.mark_for_delete, - hash_key=msg.hash_key, + force=True, ) - # Move back cur messages with appropriate message - coder.move_back_cur_messages( - f"Editor model {model_name} made those changes to the files." - ) - # Restore the original model configuration from cecli.commands import SwitchCoderSignal diff --git a/cecli/commands/model.py b/cecli/commands/model.py index 4e06c8011ef..8dd91098fb8 100644 --- a/cecli/commands/model.py +++ b/cecli/commands/model.py @@ -67,12 +67,8 @@ async def execute(cls, io, coder, args, **kwargs): temp_coder = await Coder.create(**new_kwargs) - # Clear ALL messages for temp coder (start fresh) - ConversationManager.reset() - # Re-initialize ConversationManager with temp coder - ConversationManager.initialize(temp_coder) - ConversationManager.clear_cache() + ConversationManager.initialize(temp_coder, reset=True, reformat=True) verbose = kwargs.get("verbose", False) if verbose: @@ -87,35 +83,30 @@ async def execute(cls, io, coder, args, **kwargs): temp_all_messages = ConversationManager.get_messages() # Clear manager and restore original state - ConversationManager.reset() - ConversationManager.initialize(original_coder) + ConversationManager.initialize(original_coder, reset=True, reformat=True) # Restore original messages with all metadata for msg in original_all_messages: - ConversationManager.add_message( - msg.to_dict(), - MessageTag(msg.tag), - priority=msg.priority, - timestamp=msg.timestamp, - mark_for_delete=msg.mark_for_delete, - hash_key=msg.hash_key, - ) + if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: + ConversationManager.add_message( + message_dict=msg.message_dict, + tag=MessageTag(msg.tag), + priority=msg.priority, + mark_for_delete=msg.mark_for_delete, + force=True, + ) # Append temp coder's DONE and CUR messages (but not other tags like SYSTEM) for msg in temp_all_messages: if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: ConversationManager.add_message( - msg.to_dict(), - MessageTag(msg.tag), + message_dict=msg.message_dict, + tag=MessageTag(msg.tag), priority=msg.priority, - timestamp=msg.timestamp, mark_for_delete=msg.mark_for_delete, - hash_key=msg.hash_key, + force=True, ) - # Move back cur messages with appropriate message - coder.move_back_cur_messages(f"Model {model_name} made those changes to the files.") - # Restore the original model configuration from cecli.commands import SwitchCoderSignal diff --git a/cecli/commands/reset.py b/cecli/commands/reset.py index 945bbc1fee0..79eae109b78 100644 --- a/cecli/commands/reset.py +++ b/cecli/commands/reset.py @@ -20,7 +20,7 @@ async def execute(cls, io, coder, args, **kwargs): ConversationFiles.reset() # Clear all file caches # Re-initialize ConversationManager with current coder - ConversationManager.initialize(coder) + ConversationManager.initialize(coder, reformat=True) ConversationFiles.initialize(coder) # Clear TUI output if available diff --git a/cecli/commands/utils/base_command.py b/cecli/commands/utils/base_command.py index ccbb84153be..40f4a0e84a7 100644 --- a/cecli/commands/utils/base_command.py +++ b/cecli/commands/utils/base_command.py @@ -150,12 +150,8 @@ async def _generic_chat_command(cls, io, coder, args, edit_format, placeholder=N new_coder = await Coder.create(**kwargs) - # Clear ALL messages for new coder (start fresh) - ConversationManager.reset() - # Re-initialize ConversationManager with new coder - ConversationManager.initialize(new_coder) - ConversationManager.clear_cache() + ConversationManager.initialize(new_coder, reset=True, reformat=True) await new_coder.generate(user_message=user_msg, preproc=False) coder.coder_commit_hashes = new_coder.coder_commit_hashes @@ -164,30 +160,28 @@ async def _generic_chat_command(cls, io, coder, args, edit_format, placeholder=N new_all_messages = ConversationManager.get_messages() # Clear manager and restore original state - ConversationManager.reset() - ConversationManager.initialize(original_coder) + ConversationManager.initialize(original_coder, reset=True, reformat=True) # Restore original messages with all metadata for msg in original_all_messages: - ConversationManager.add_message( - msg.to_dict(), - MessageTag(msg.tag), - priority=msg.priority, - timestamp=msg.timestamp, - mark_for_delete=msg.mark_for_delete, - hash_key=msg.hash_key, - ) + if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: + ConversationManager.add_message( + message_dict=msg.message_dict, + tag=MessageTag(msg.tag), + priority=msg.priority, + mark_for_delete=msg.mark_for_delete, + force=True, + ) # Append new coder's DONE and CUR messages (but not other tags like SYSTEM) for msg in new_all_messages: if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: ConversationManager.add_message( - msg.to_dict(), - MessageTag(msg.tag), + message_dict=msg.message_dict, + tag=MessageTag(msg.tag), priority=msg.priority, - timestamp=msg.timestamp, mark_for_delete=msg.mark_for_delete, - hash_key=msg.hash_key, + force=True, ) from cecli.commands import SwitchCoderSignal diff --git a/cecli/commands/weak_model.py b/cecli/commands/weak_model.py index 973b44bf9a2..ededbe330f4 100644 --- a/cecli/commands/weak_model.py +++ b/cecli/commands/weak_model.py @@ -62,12 +62,8 @@ async def execute(cls, io, coder, args, **kwargs): temp_coder = await Coder.create(**new_kwargs) - # Clear ALL messages for temp coder (start fresh) - ConversationManager.reset() - # Re-initialize ConversationManager with temp coder - ConversationManager.initialize(temp_coder) - ConversationManager.clear_cache() + ConversationManager.initialize(temp_coder, reset=True, reformat=True) verbose = kwargs.get("verbose", False) if verbose: @@ -82,37 +78,30 @@ async def execute(cls, io, coder, args, **kwargs): temp_all_messages = ConversationManager.get_messages() # Clear manager and restore original state - ConversationManager.reset() - ConversationManager.initialize(original_coder) + ConversationManager.initialize(original_coder, reset=True, reformat=True) # Restore original messages with all metadata for msg in original_all_messages: - ConversationManager.add_message( - msg.to_dict(), - MessageTag(msg.tag), - priority=msg.priority, - timestamp=msg.timestamp, - mark_for_delete=msg.mark_for_delete, - hash_key=msg.hash_key, - ) + if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: + ConversationManager.add_message( + message_dict=msg.message_dict, + tag=MessageTag(msg.tag), + priority=msg.priority, + mark_for_delete=msg.mark_for_delete, + force=True, + ) # Append temp coder's DONE and CUR messages (but not other tags like SYSTEM) for msg in temp_all_messages: if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: ConversationManager.add_message( - msg.to_dict(), - MessageTag(msg.tag), + message_dict=msg.message_dict, + tag=MessageTag(msg.tag), priority=msg.priority, - timestamp=msg.timestamp, mark_for_delete=msg.mark_for_delete, - hash_key=msg.hash_key, + force=True, ) - # Move back cur messages with appropriate message - coder.move_back_cur_messages( - f"Weak model {model_name} made those changes to the files." - ) - # Restore the original model configuration from cecli.commands import SwitchCoderSignal diff --git a/cecli/helpers/conversation/manager.py b/cecli/helpers/conversation/manager.py index 43c4713f4f3..0e6c0155b76 100644 --- a/cecli/helpers/conversation/manager.py +++ b/cecli/helpers/conversation/manager.py @@ -33,16 +33,26 @@ class ConversationManager: _ALL_MESSAGES_CACHE_KEY = "__all__" # Special key for caching all messages (tag=None) @classmethod - def initialize(cls, coder) -> None: + def initialize(cls, coder, reset: bool = False, reformat: bool = False) -> None: """ Set up singleton with weak reference to coder. Args: coder: The coder instance to reference + reset: Whether to re-initialize the conversation history itself + reformat: Whether to format chat history + (useful for initialization outside of coder class) """ cls._coder_ref = weakref.ref(coder) cls._initialized = True + if reset: + cls.reset() + + if reformat: + if hasattr(coder, "format_chat_chunks"): + coder.format_chat_chunks() + # Enable debug mode if coder has verbose attribute and it's True if hasattr(coder, "verbose") and coder.verbose: cls._debug_enabled = True diff --git a/cecli/main.py b/cecli/main.py index 7cec8ac83d0..4ab8833cb57 100644 --- a/cecli/main.py +++ b/cecli/main.py @@ -42,7 +42,11 @@ from cecli.commands import Commands, SwitchCoderSignal from cecli.deprecated_args import handle_deprecated_model_args from cecli.format_settings import format_settings, scrub_sensitive_info -from cecli.helpers.conversation import ConversationChunks +from cecli.helpers.conversation import ( + ConversationChunks, + ConversationManager, + MessageTag, +) from cecli.helpers.copypaste import ClipboardWatcher from cecli.helpers.file_searcher import generate_search_path_list from cecli.history import ChatSummary @@ -1215,6 +1219,9 @@ def apply_model_overrides(model_name): if coder.mcp_manager.get_server("Local"): await coder.mcp_manager.disconnect_server("Local") + for tag in [MessageTag.SYSTEM, MessageTag.EXAMPLES, MessageTag.STATIC]: + ConversationManager.clear_tag(tag) + coder = await Coder.create(**kwargs) if switch.kwargs.get("show_announcements") is False: diff --git a/cecli/mcp/manager.py b/cecli/mcp/manager.py index 58524f768d5..60c2a2dbd52 100644 --- a/cecli/mcp/manager.py +++ b/cecli/mcp/manager.py @@ -186,7 +186,8 @@ async def add_server(self, server: McpServer, connect: bool = False) -> bool: """ existing_server = self.get_server(server.name) if existing_server: - self._log_warning(f"MCP server with name '{server.name}' already exists") + if server.name not in ["unnamed-server", "Local"]: + self._log_warning(f"MCP server with name '{server.name}' already exists") return False self._servers.append(server) diff --git a/cecli/tui/app.py b/cecli/tui/app.py index 6168c742813..a964541659e 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -605,6 +605,11 @@ def on_input_area_submit(self, message: InputArea.Submit): footer = self.query_one(MainFooter) footer.start_spinner("Processing...") + coder = self.worker.coder + + if coder: + coder.io.start_spinner("Processing...") + self.update_key_hints(generating=True) self.input_queue.put({"text": user_input}) diff --git a/cecli/tui/worker.py b/cecli/tui/worker.py index 68c08775b0e..cfa2acc0a42 100644 --- a/cecli/tui/worker.py +++ b/cecli/tui/worker.py @@ -8,6 +8,7 @@ from cecli.coders import Coder from cecli.commands import SwitchCoderSignal +from cecli.helpers.conversation import ConversationManager, MessageTag # Suppress asyncio task destroyed warnings during shutdown logging.getLogger("asyncio").setLevel(logging.CRITICAL) @@ -103,6 +104,9 @@ async def _async_run(self): # Skip summarization to avoid blocking LLM calls during mode switch kwargs["summarize_from_coder"] = False + for tag in [MessageTag.SYSTEM, MessageTag.EXAMPLES, MessageTag.STATIC]: + ConversationManager.clear_tag(tag) + new_coder = await Coder.create(**kwargs) new_coder.args = self.coder.args